├── .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 | 6 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 10 | 11 | 13 | 14 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 7 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/jsonSchemas.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Bump_Versions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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(Track.serializer()) 15 | 16 | abstract class JsonElementSerializer( 17 | private val serializer: KSerializer, 18 | ) : KSerializer { 19 | override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("JsonStringSerializer", PrimitiveKind.STRING) 20 | 21 | override fun deserialize(decoder: Decoder): T = Json.decodeFromString(serializer, decoder.decodeString()) 22 | override fun serialize(encoder: Encoder, value: T) = 23 | encoder.encodeString(Json.encodeToString(serializer, value)) 24 | } 25 | -------------------------------------------------------------------------------- /music/player/src/main/kotlin/dev/schlaubi/mikmusic/util/LinkedListSerializer.kt: -------------------------------------------------------------------------------- 1 | package dev.schlaubi.mikmusic.util 2 | 3 | import kotlinx.serialization.KSerializer 4 | import kotlinx.serialization.builtins.ListSerializer 5 | import kotlinx.serialization.descriptors.SerialDescriptor 6 | import kotlinx.serialization.encoding.Decoder 7 | import kotlinx.serialization.encoding.Encoder 8 | import java.util.* 9 | 10 | class LinkedListSerializer(elementSerializer: KSerializer) : KSerializer> { 11 | private val parent: KSerializer> = ListSerializer(elementSerializer) 12 | override val descriptor: SerialDescriptor 13 | get() = parent.descriptor 14 | override fun deserialize(decoder: Decoder): LinkedList = LinkedList(parent.deserialize(decoder)) 15 | 16 | override fun serialize(encoder: Encoder, value: LinkedList) = parent.serialize(encoder, value) 17 | } 18 | -------------------------------------------------------------------------------- /music/player/src/main/kotlin/dev/schlaubi/mikmusic/util/QueuedTrackUtl.kt: -------------------------------------------------------------------------------- 1 | package dev.schlaubi.mikmusic.util 2 | 3 | import dev.arbjerg.lavalink.protocol.v4.Track 4 | import dev.kord.core.behavior.UserBehavior 5 | import dev.schlaubi.mikmusic.api.types.SimpleQueuedTrack 6 | 7 | fun List.mapToQueuedTrack(user: UserBehavior) = map { SimpleQueuedTrack(it, user.id) } 8 | -------------------------------------------------------------------------------- /music/player/src/main/kotlin/dev/schlaubi/mikmusic/util/SpotifyUtil.kt: -------------------------------------------------------------------------------- 1 | package dev.schlaubi.mikmusic.util 2 | 3 | import dev.arbjerg.lavalink.protocol.v4.Track 4 | 5 | val Track.spotifyId: String? 6 | get() = if(info.sourceName == "spotify") info.identifier else null 7 | -------------------------------------------------------------------------------- /music/player/src/main/kotlin/dev/schlaubi/mikmusic/util/TrackSerialization.kt: -------------------------------------------------------------------------------- 1 | package dev.schlaubi.mikmusic.util 2 | 3 | import dev.arbjerg.lavalink.protocol.v4.Track 4 | import dev.schlaubi.lavakord.audio.Node 5 | import dev.schlaubi.lavakord.rest.decodeTrack 6 | import kotlinx.serialization.KSerializer 7 | import kotlinx.serialization.Serializable 8 | import kotlinx.serialization.builtins.ListSerializer 9 | import java.util.LinkedList 10 | 11 | @JvmInline 12 | @Serializable 13 | value class EncodedTrack(val value: String) { 14 | suspend fun toTrack(lavalink: Node): Track = lavalink.decodeTrack(value) 15 | } 16 | 17 | object TrackListSerializer : KSerializer> by ListSerializer(EncodedTrack.serializer()) 18 | object TrackLinkedListSerializer : KSerializer> by LinkedListSerializer(EncodedTrack.serializer()) 19 | 20 | fun List.mapToEncoded(): List = map(Track::toEncodedTrack) 21 | 22 | fun Track.toEncodedTrack() = EncodedTrack(encoded) 23 | -------------------------------------------------------------------------------- /music/player/src/main/kotlin/dev/schlaubi/mikmusic/util/TrackUtil.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 kotlin.time.DurationUnit 6 | import kotlin.time.toDuration 7 | 8 | /** 9 | * Formats a simple message for a [Track]. 10 | * 11 | * @param repeat whether to add the repeat emoji or not 12 | */ 13 | fun QueuedTrack.format(repeat: Boolean = false) = with(track.info) { 14 | "[`$title - $author`]($uri) (${length.toDuration(DurationUnit.MILLISECONDS)}) (<@$queuedBy>)".run { 15 | if (repeat) { 16 | "🔂 $this" 17 | } else { 18 | this 19 | } 20 | } 21 | } 22 | 23 | /** 24 | * Formats a simple message for a [Track]. 25 | * 26 | * @param repeat whether to add the repeat emoji or not 27 | */ 28 | fun Track.format(repeat: Boolean = false) = with(info) { 29 | "[`$title - $author`]($uri) (${length.toDuration(DurationUnit.MILLISECONDS)})".run { 30 | if (repeat) { 31 | "🔂 $this" 32 | } else { 33 | this 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /music/player/src/main/resources/translations/music/strings_it_IT.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DRSchlaubi/mikbot/d30c1849f6f6ceed028c4ce3afc418f5bcc97a2e/music/player/src/main/resources/translations/music/strings_it_IT.properties -------------------------------------------------------------------------------- /music/player/src/main/resources/translations/music/strings_nb_NO.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DRSchlaubi/mikbot/d30c1849f6f6ceed028c4ce3afc418f5bcc97a2e/music/player/src/main/resources/translations/music/strings_nb_NO.properties -------------------------------------------------------------------------------- /music/player/src/main/resources/translations/music/strings_pl.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DRSchlaubi/mikbot/d30c1849f6f6ceed028c4ce3afc418f5bcc97a2e/music/player/src/main/resources/translations/music/strings_pl.properties -------------------------------------------------------------------------------- /music/player/src/main/resources/translations/music/strings_vi.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DRSchlaubi/mikbot/d30c1849f6f6ceed028c4ce3afc418f5bcc97a2e/music/player/src/main/resources/translations/music/strings_vi.properties -------------------------------------------------------------------------------- /plugin-processor/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `mikbot-module` 3 | `mikbot-publishing` 4 | } 5 | 6 | group = "dev.schlaubi" 7 | version = mikbotVersion 8 | 9 | dependencies { 10 | implementation(libs.ksp.api) 11 | implementation(projects.api.annotations) 12 | implementation(libs.pf4j) 13 | } 14 | -------------------------------------------------------------------------------- /plugin-processor/src/main/kotlin/dev/schlaubi/mikbot/plugin/processor/PluginProcessorProvider.kt: -------------------------------------------------------------------------------- 1 | package dev.schlaubi.mikbot.plugin.processor 2 | 3 | import com.google.devtools.ksp.processing.SymbolProcessor 4 | import com.google.devtools.ksp.processing.SymbolProcessorEnvironment 5 | import com.google.devtools.ksp.processing.SymbolProcessorProvider 6 | 7 | class PluginProcessorProvider : SymbolProcessorProvider { 8 | override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor = PluginProcessor(environment) 9 | } 10 | -------------------------------------------------------------------------------- /plugin-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider: -------------------------------------------------------------------------------- 1 | dev.schlaubi.mikbot.plugin.processor.PluginProcessorProvider 2 | -------------------------------------------------------------------------------- /rebuild-plugin-dependency-list.ps1: -------------------------------------------------------------------------------- 1 | ./gradlew runtime:exportDependencies :gradle-plugin:clean 2 | Remove-Item -Recurse buildSrc/build 3 | ./gradlew 4 | -------------------------------------------------------------------------------- /rebuild-plugin-dependency-list.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | ./gradlew runtime:exportDependencies :gradle-plugin:clean 3 | rm -rf buildSrc/build 4 | ./gradlew 5 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "prHourlyLimit": 6, 6 | "packageRules": [ 7 | { 8 | "matchPackageNames": ["/mikbot/", "gradle-plugin"], 9 | "matchManagers": ["gradle"], 10 | "enabled": false 11 | } 12 | ], 13 | } 14 | -------------------------------------------------------------------------------- /runtime/plugins.txt: -------------------------------------------------------------------------------- 1 | :core:ktor 2 | :core:redeploy-hook 3 | :core:kubernetes 4 | :music:player 5 | :music:commands 6 | -------------------------------------------------------------------------------- /runtime/src/README.md: -------------------------------------------------------------------------------- 1 | # Main bot source 2 | 3 | This folder houses the source code of the actual bot. 4 | -------------------------------------------------------------------------------- /runtime/src/main/kotlin/dev/schlaubi/musicbot/Launcher.kt: -------------------------------------------------------------------------------- 1 | package dev.schlaubi.musicbot 2 | 3 | import ch.qos.logback.classic.Logger 4 | import dev.schlaubi.mikbot.plugin.api._pluginSystem 5 | import dev.schlaubi.mikbot.plugin.api.config.Config 6 | import dev.schlaubi.musicbot.core.Bot 7 | import dev.schlaubi.musicbot.core.plugin.MikBotPluginRepository 8 | import io.ktor.http.* 9 | import org.slf4j.LoggerFactory 10 | import kotlin.io.path.absolutePathString 11 | import kotlin.io.path.createDirectories 12 | import kotlin.io.path.exists 13 | 14 | suspend fun main() { 15 | initializeLogging() 16 | val repos = Config.PLUGIN_REPOSITORIES.map { 17 | MikBotPluginRepository(Url(it)) 18 | } 19 | val bot = Bot(repos) 20 | loadPlugins(bot) 21 | bot.start() 22 | } 23 | 24 | private fun loadPlugins(bot: Bot) { 25 | if (System.getProperty("pf4j.pluginsDir").isNullOrBlank()) { 26 | System.setProperty("pf4j.pluginsDir", Config.PLUGIN_PATH.absolutePathString()) 27 | } 28 | 29 | if (!Config.PLUGIN_PATH.exists()) { 30 | Config.PLUGIN_PATH.createDirectories() 31 | } 32 | 33 | _pluginSystem = bot.pluginSystem 34 | bot.pluginLoader.loadPlugins() 35 | bot.pluginLoader.startPlugins() 36 | } 37 | 38 | private fun initializeLogging() { 39 | val rootLogger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) as? Logger 40 | if (rootLogger == null) { 41 | LoggerFactory.getLogger("MikBot").warn("Could not set log level due to different logging engine being used") 42 | return 43 | } 44 | rootLogger.level = Config.LOG_LEVEL 45 | } 46 | -------------------------------------------------------------------------------- /runtime/src/main/kotlin/dev/schlaubi/musicbot/core/AboutCommand.kt: -------------------------------------------------------------------------------- 1 | package dev.schlaubi.musicbot.core 2 | 3 | import dev.kordex.core.builders.AboutBuilder 4 | import dev.kordex.core.builders.about.CopyrightType 5 | import dev.schlaubi.mikbot.plugin.api.AboutExtensionPoint 6 | import dev.schlaubi.mikbot.plugin.api.getExtensions 7 | import kotlinx.serialization.ExperimentalSerializationApi 8 | import kotlinx.serialization.Serializable 9 | import kotlinx.serialization.json.Json 10 | import kotlinx.serialization.json.decodeFromStream 11 | import org.pf4j.PluginState 12 | import java.io.InputStream 13 | 14 | @Serializable 15 | private data class LicenseReport( 16 | val dependencies: List = emptyList(), 17 | ) { 18 | @Suppress("unused") 19 | @Serializable 20 | class Dependency( 21 | val moduleName: String, 22 | val moduleUrl: String? = null, 23 | val moduleVersion: String, 24 | val moduleLicense: String? = null, 25 | val moduleLicenseUrl: String? = null, 26 | ) 27 | } 28 | 29 | @OptIn(ExperimentalSerializationApi::class) 30 | suspend fun AboutBuilder.aboutCommand(bot: Bot) { 31 | copyright("MikBot", "MIT", CopyrightType.Framework, "https://github.com/DRSchlaubi/mikbot") 32 | 33 | bot.pluginLoader.plugins.asSequence() 34 | .filter { it.pluginState != PluginState.DISABLED } 35 | .mapNotNull { it.pluginClassLoader.getResourceAsStream("license-report.json") } 36 | .map { it.use(Json.Default::decodeFromStream) } 37 | .flatMap(LicenseReport::dependencies) 38 | .distinctBy(LicenseReport.Dependency::moduleName) 39 | .forEach { 40 | copyright(it.moduleName, it.moduleLicense ?: "Unknown License", CopyrightType.Library, it.moduleUrl) 41 | } 42 | 43 | bot.pluginSystem.getExtensions().forEach { 44 | with(it) { apply() } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /runtime/src/main/kotlin/dev/schlaubi/musicbot/core/io/DatabaseImpl.kt: -------------------------------------------------------------------------------- 1 | package dev.schlaubi.musicbot.core.io 2 | 3 | import dev.schlaubi.mikbot.plugin.api.config.Config 4 | import dev.schlaubi.mikbot.plugin.api.io.Database 5 | import dev.schlaubi.mikbot.plugin.api.util.IKnowWhatIAmDoing 6 | import org.litote.kmongo.coroutine.CoroutineClient 7 | import org.litote.kmongo.coroutine.CoroutineDatabase 8 | import org.litote.kmongo.coroutine.coroutine 9 | import org.litote.kmongo.reactivestreams.KMongo 10 | import org.litote.kmongo.serialization.registerSerializer 11 | 12 | class DatabaseImpl : Database { 13 | 14 | private var internalClient: CoroutineClient? = null 15 | private var internalDatabase: CoroutineDatabase? = null 16 | 17 | init { 18 | val url = Config.MONGO_URL 19 | val database = Config.MONGO_DATABASE 20 | 21 | if (url != null && database != null) { 22 | registerSerializer(DurationSerializer) 23 | internalClient = KMongo.createClient(url).coroutine 24 | 25 | internalDatabase = internalClient?.getDatabase(database) 26 | } 27 | } 28 | 29 | override val database: CoroutineDatabase 30 | get() = internalDatabase 31 | ?: error("Database connection is not ready on this instance, please define MONGO_URL and MONGO_DATABASE") 32 | 33 | @IKnowWhatIAmDoing 34 | override val client: CoroutineClient 35 | get() = internalClient 36 | ?: error("Database connection is not ready on this instance, please define MONGO_URL and MONGO_DATABASE") 37 | } 38 | -------------------------------------------------------------------------------- /runtime/src/main/kotlin/dev/schlaubi/musicbot/core/io/DurationSerializer.kt: -------------------------------------------------------------------------------- 1 | package dev.schlaubi.musicbot.core.io 2 | 3 | import kotlinx.serialization.KSerializer 4 | import kotlinx.serialization.descriptors.PrimitiveKind 5 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 6 | import kotlinx.serialization.descriptors.SerialDescriptor 7 | import kotlinx.serialization.encoding.Decoder 8 | import kotlinx.serialization.encoding.Encoder 9 | import kotlin.time.Duration 10 | import kotlin.time.Duration.Companion.milliseconds 11 | 12 | object DurationSerializer : KSerializer { 13 | override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("MikBotDuration", PrimitiveKind.LONG) 14 | 15 | override fun deserialize(decoder: Decoder): Duration = decoder.decodeLong().milliseconds 16 | 17 | override fun serialize(encoder: Encoder, value: Duration): Unit = encoder.encodeLong(value.inWholeMilliseconds) 18 | } 19 | -------------------------------------------------------------------------------- /runtime/src/main/kotlin/dev/schlaubi/musicbot/core/plugin/KtorHttpFileDownloader.kt: -------------------------------------------------------------------------------- 1 | package dev.schlaubi.musicbot.core.plugin 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.call.* 5 | import io.ktor.client.request.* 6 | import io.ktor.http.* 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.runBlocking 9 | import kotlinx.coroutines.withContext 10 | import org.pf4j.update.FileDownloader 11 | import java.net.URL 12 | import java.nio.file.Files 13 | import java.nio.file.Path 14 | import kotlin.io.path.createFile 15 | import kotlin.io.path.div 16 | import kotlin.io.path.writeBytes 17 | 18 | object KtorHttpFileDownloader : FileDownloader { 19 | private val client = HttpClient() 20 | 21 | override fun downloadFile(fileUrl: URL): Path = runBlocking { 22 | val destination = withContext(Dispatchers.IO) { 23 | Files.createTempDirectory("pf4j-update-downloader") 24 | } 25 | val url = URLBuilder(fileUrl.toString()).apply { 26 | encodedPath = encodedPath.replace("//", "/") 27 | }.build() 28 | 29 | val bytes = client.get(url).body() 30 | 31 | val downloadedFile = destination / url.encodedPath.substringAfterLast('/') 32 | 33 | downloadedFile.createFile() 34 | downloadedFile.writeBytes(bytes) 35 | 36 | downloadedFile 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /runtime/src/main/kotlin/dev/schlaubi/musicbot/core/plugin/MikbotPluginFactory.kt: -------------------------------------------------------------------------------- 1 | package dev.schlaubi.musicbot.core.plugin 2 | 3 | import dev.schlaubi.mikbot.plugin.api.PluginContext 4 | import dev.schlaubi.mikbot.plugin.api.PluginSystem 5 | import dev.schlaubi.mikbot.plugin.api.io.Database 6 | import dev.schlaubi.musicbot.core.Bot 7 | import mu.KotlinLogging 8 | import org.pf4j.DefaultPluginFactory 9 | import org.pf4j.Plugin 10 | import org.pf4j.PluginWrapper 11 | import kotlin.reflect.typeOf 12 | 13 | private val LOG = KotlinLogging.logger { } 14 | 15 | class MikbotPluginFactory(private val bot: Bot) : DefaultPluginFactory() { 16 | override fun createInstance(pluginClass: Class<*>, pluginWrapper: PluginWrapper): Plugin? { 17 | val newConstructor = pluginClass.kotlin.constructors.firstOrNull { 18 | it.parameters.size == 1 && it.parameters.first().type == typeOf() 19 | } 20 | return if (newConstructor == null) { 21 | // legacy implementation 22 | super.createInstance(pluginClass, pluginWrapper) 23 | } else { 24 | try { 25 | val context = PluginContextImpl(bot.pluginSystem, bot.database, pluginWrapper) 26 | newConstructor.call(context) as Plugin 27 | } catch (e: Exception) { 28 | LOG.error("Could not instantiate plugin: ${pluginWrapper.pluginId}", e) 29 | null 30 | } 31 | } 32 | } 33 | } 34 | 35 | private class PluginContextImpl( 36 | override val pluginSystem: PluginSystem, 37 | override val database: Database, 38 | override val pluginWrapper: PluginWrapper, 39 | ) : PluginContext 40 | -------------------------------------------------------------------------------- /runtime/src/main/kotlin/dev/schlaubi/musicbot/core/plugin/PluginTranslationProvider.kt: -------------------------------------------------------------------------------- 1 | package dev.schlaubi.musicbot.core.plugin 2 | 3 | import dev.kordex.core.i18n.ResourceBundleTranslations 4 | import dev.kordex.core.i18n.TranslationsProvider 5 | import dev.kord.common.asJavaLocale 6 | import dev.kord.common.kLocale 7 | import dev.schlaubi.mikbot.plugin.api.util.convertToISO 8 | import mu.KotlinLogging 9 | import java.util.* 10 | 11 | private val LOG = KotlinLogging.logger { } 12 | 13 | /** 14 | * Implementation of [TranslationsProvider] handling different plugin class loaders. 15 | */ 16 | class PluginTranslationProvider(private val pluginLoader: PluginLoader, defaultLocaleBuilder: () -> Locale) : ResourceBundleTranslations(defaultLocaleBuilder) { 17 | override fun getResourceBundle(bundle: String, locale: Locale, control: ResourceBundle.Control): ResourceBundle { 18 | val plugin = pluginLoader.getPluginForBundle(bundle) 19 | val classLoader = 20 | plugin?.pluginClassLoader ?: ClassLoader.getSystemClassLoader() 21 | LOG.debug { "Found classloader for $bundle to be $classLoader (${plugin?.pluginId ?: ""})" } 22 | 23 | return ResourceBundle.getBundle(bundle, locale.kLocale.convertToISO().asJavaLocale(), classLoader, control) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /runtime/src/main/kotlin/dev/schlaubi/musicbot/core/plugin/Utils.kt: -------------------------------------------------------------------------------- 1 | package dev.schlaubi.musicbot.core.plugin 2 | 3 | import dev.schlaubi.mikbot.plugin.api.Plugin 4 | import org.pf4j.Plugin as PF4JPlugin 5 | 6 | fun PF4JPlugin.asPlugin(): Plugin = this as? Plugin ?: error("Invalid plugin detected: $this") 7 | -------------------------------------------------------------------------------- /runtime/src/main/kotlin/dev/schlaubi/musicbot/core/sentry/SentryExtensionPoint.kt: -------------------------------------------------------------------------------- 1 | package dev.schlaubi.musicbot.core.sentry 2 | 3 | import io.sentry.Breadcrumb 4 | import io.sentry.Hint 5 | import io.sentry.SentryEvent 6 | import io.sentry.SentryOptions 7 | import org.pf4j.ExtensionPoint 8 | 9 | /** 10 | * Allows plugins to customize sentry. 11 | */ 12 | interface SentryExtensionPoint : ExtensionPoint { 13 | /** 14 | * Apply custom sentry options in the setup. 15 | * 16 | * DO NOT SET [SentryOptions.beforeSend] or [SentryOptions.beforeBreadcrumb] here! 17 | * IT WON'T WORK 18 | */ 19 | fun SentryOptions.setup() 20 | 21 | /** 22 | * Allows plugins to listen to the beforeSend Sentry event. 23 | */ 24 | fun beforeSend(sentryEvent: SentryEvent, hint: Hint) 25 | 26 | /** 27 | * Allows plugins to listen to the beforeBreadcrumb Sentry event. 28 | */ 29 | fun beforeBreadcrumb(breadcrumb: Breadcrumb, hint: Hint) 30 | } 31 | -------------------------------------------------------------------------------- /runtime/src/main/kotlin/dev/schlaubi/musicbot/module/owner/OwnerModuleImpl.kt: -------------------------------------------------------------------------------- 1 | package dev.schlaubi.musicbot.module.owner 2 | 3 | import dev.kordex.core.extensions.slashCommandCheck 4 | import dev.schlaubi.mikbot.plugin.api.MikBotTranslations 5 | import dev.schlaubi.mikbot.plugin.api.ModuleExtensionPoint 6 | import dev.schlaubi.mikbot.plugin.api.PluginContext 7 | import dev.schlaubi.mikbot.plugin.api.config.Config 8 | import dev.schlaubi.mikbot.plugin.api.io.Database 9 | import dev.schlaubi.mikbot.plugin.api.owner.OwnerExtensionPoint 10 | import dev.schlaubi.mikbot.plugin.api.owner.OwnerModule 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.SupervisorJob 13 | import kotlinx.coroutines.cancel 14 | import org.koin.core.component.inject 15 | import kotlin.coroutines.CoroutineContext 16 | import kotlin.reflect.KClass 17 | 18 | class OwnerModuleImpl(pluginSystem: PluginContext) : OwnerModule(pluginSystem) { 19 | override val name: String = "owner" 20 | val database: Database by inject() 21 | override val coroutineContext: CoroutineContext = Dispatchers.IO + SupervisorJob() 22 | override val extensionClazz: KClass> = OwnerExtensionPoint::class 23 | 24 | override suspend fun setup() { 25 | slashCommandCheck { 26 | failIfNot(MikBotTranslations.Checks.Owner.failed) { event.interaction.user.id in Config.BOT_OWNERS } 27 | } 28 | 29 | super.setup() 30 | } 31 | 32 | override suspend fun unload() { 33 | coroutineContext.cancel() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /runtime/src/main/kotlin/dev/schlaubi/musicbot/module/settings/SettingsModuleImpl.kt: -------------------------------------------------------------------------------- 1 | package dev.schlaubi.musicbot.module.settings 2 | 3 | import dev.schlaubi.mikbot.plugin.api.ModuleExtensionPoint 4 | import dev.schlaubi.mikbot.plugin.api.PluginContext 5 | import dev.schlaubi.mikbot.plugin.api.io.Database 6 | import dev.schlaubi.mikbot.plugin.api.settings.SettingsExtensionPoint 7 | import dev.schlaubi.mikbot.plugin.api.settings.SettingsModule 8 | import org.koin.core.component.inject 9 | import kotlin.reflect.KClass 10 | 11 | class SettingsModuleImpl(pluginSystem: PluginContext) : SettingsModule(pluginSystem) { 12 | override val name: String = "settings" 13 | override val extensionClazz: KClass> = SettingsExtensionPoint::class 14 | val database: Database by inject() 15 | } 16 | -------------------------------------------------------------------------------- /runtime/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /runtime/src/main/resources/translations/settings/strings_de_DE.properties: -------------------------------------------------------------------------------- 1 | commands.music_channel.notextchannel=`{0}` ist kein Text-Kanal. 2 | settings.musicchannel.confirmnew=Bist du dir sicher, dass du einen neuen Musik-Kanal setzen willst? 3 | settings.musicchannel.new.aborted=Neuen Musik-Kanal setzen abgebrochen. 4 | settings.musicchannel.createdchannel=Musik-Kanal wurde erstellt. 5 | command.music_channel.channel_missing_perms=Du musst mir das Recht `MESSAGE_MANAGE` f�r diesen Kanal geben. Bitte. 6 | settings.loading=lade... 7 | settings.musicchannel.try_delete_messages=Es scheinen schon Nachrichten im Musikkanal zu sein. Soll ich versuchen, sie zu l�schen? 8 | music.multiple_scheduler_options=Du kannst nur eins von 'shuffle', 'repeat' oder 'loopqueue' ausw�hlen. Willst du die derzeitige Einstellung �berschreiben? (Falls nicht, kannst du diese Nachricht einfach l�schen) 9 | command.djmode.enabled=Du hast den DJ Mode erfolgreich eingeschaltet und ihn auf '{0}' gesetzt. 10 | command.djmode.disabled=Du hast den DJ Mode erfolgreich ausgeschaltet. 11 | command.sponsorblock.enabled=Du hast SponsorBlock erfolgreich eingeschaltet. 12 | command.sponsorblock.disabled=Du hast SponsorBlock erfolgreich ausgeschaltet. 13 | commands.leave_timeout.success=Der Bot wird nun `{0, time}` nachdem die Musik gestoppt hat, den Channel verlassen. 14 | command.leave_timeout.limit_exceeded=Bist du verr�ckt? Ich kann doch nur bis 1 Googol z�hlen, bitte gebe nichts \ 15 | h�heres als `{0, time}` an. 16 | -------------------------------------------------------------------------------- /runtime/src/main/resources/translations/settings/strings_en_GB.properties: -------------------------------------------------------------------------------- 1 | commands.music_channel.notextchannel=`{0}` is not a valid text channel. 2 | settings.musicchannel.confirmnew=Are you sure you want to set a new Music Channel? 3 | settings.musicchannel.new.aborted=Aborted settings new Music Channel. 4 | settings.musicchannel.createdchannel=Music channel was created. 5 | command.music_channel.channel_missing_perms=Please give me the `MESSAGE_MANAGE` permission for the specified channel. 6 | settings.loading=Loading ... 7 | settings.musicchannel.try_delete_messages=There already seem to be messages in this channel, should I try to delete them? 8 | music.multiple_scheduler_options=You can only enable either shuffle, repeat or loopqueue. Do you want to overwrite these settings?, Just delete this message if you don't. 9 | command.verify.unknown_id=The specified ID doesn't match a guild. 10 | command.verify.confirm=You're about to disable the verification status of `{0}`. Do you want to continue? 11 | command.verify.success=Successfully disabled the verification status of `{0}`. 12 | command.verify.not_verified=This guild is not verified. 13 | command.djmode.enabled=You successfully enabled the DJ Mode and set it to '{0}'. 14 | command.djmode.disabled=You successfully disabled the DJ Mode. 15 | command.scheduler_settings.updated=Your default settings have been successfully updated. 16 | commands.fix_music_channel.not_enabled=This server doesn't have music channel enabled. 17 | command.sponsorblock.enabled=You successfully enabled SponsorBlock. 18 | command.sponsorblock.disabled=You successfully disabled SponsorBlock. 19 | commands.leave_timeout.success=The bot will now stop leaving the channel `{0, time}` after the queue is finished. 20 | command.leave_timeout.limit_exceeded=Let's stay within reason, the limit is `{0, time}`. 21 | -------------------------------------------------------------------------------- /scripts/bump_versions.main.kts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env kotlin 2 | @file:DependsOn( 3 | "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10", 4 | "com.vdurmont:semver4j:3.1.0" 5 | ) 6 | 7 | import com.vdurmont.semver4j.Semver 8 | import java.nio.file.Files 9 | import kotlin.io.path.* 10 | import kotlin.streams.asSequence 11 | 12 | val blacklist = listOf("test-bot", "mikmusic-bot", "gradle-plugin", "plugin-processor", "image-color-client", "google-emotes") 13 | 14 | val rootDir = Path("").absolute() 15 | 16 | val regexes = listOf( 17 | Regex("version = \\\"([\\w\\.]+)\\\""), 18 | Regex("\"([\\w.]+)\"\\s+//\\sversion marker") 19 | ) 20 | 21 | Files.walk(rootDir) 22 | .asSequence() 23 | .filter { it.isDirectory() } 24 | .filterNot { blacklist.any { blacklist -> blacklist in it.relativeTo(rootDir).toString() } } 25 | .forEach { path -> 26 | path.listDirectoryEntries("{build.gradle.kts,Project.kt}").forEach { file -> 27 | val content = file.readText() 28 | regexes.mapNotNull { regex -> 29 | regex.find(content) 30 | }.forEach { result -> 31 | val (version) = result.destructured 32 | val semver = Semver(version).nextMinor() 33 | println( 34 | "Bumped %s from %s to %s".format( 35 | file.relativeTo(rootDir).toString(), 36 | version, 37 | semver.toString() 38 | ) 39 | ) 40 | val newContent = content.replace(result.value, result.value.replace(version, semver.toString())) 41 | file.writeText(newContent) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.gradle.develocity") version "3.18.1" 3 | } 4 | 5 | develocity { 6 | buildScan { 7 | termsOfUseUrl = "https://gradle.com/terms-of-service" 8 | termsOfUseAgree = "yes" 9 | } 10 | } 11 | 12 | rootProject.name = "mikmusic" 13 | 14 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 15 | 16 | if (System.getenv("BUILD_PLUGIN_CI")?.toBoolean() != true) { 17 | include( 18 | "api", 19 | "api:annotations", 20 | "plugin-processor", 21 | "runtime", 22 | "core:database-i18n", 23 | "core:game-animator", 24 | "core:gdpr", 25 | "core:kubernetes", 26 | "core:redeploy-hook", 27 | "core:ktor", 28 | ":music", 29 | "music:player", 30 | "music:commands", 31 | "music:lyrics", 32 | "music:api", 33 | "music:api:types", 34 | "music:api:server", 35 | "clients:discord-oauth", 36 | "clients:haste-client", 37 | "clients:image-color-client", 38 | "clients:image-color-client-kord" 39 | ) 40 | } 41 | 42 | includeBuild("gradle-plugin") 43 | 44 | buildCache { 45 | remote { 46 | isPush = (System.getenv("GRADLE_BUILD_CACHE_PUSH") == "true") && (System.getenv("IS_PR") == "false") 47 | url = uri("https://gradle-build-cache.srv02.schlaubi.net/cache/") 48 | val cacheUsername = System.getenv("GRADLE_BUILDCACHE_USERNNAME") 49 | val cachePassword = System.getenv("GRADLE_BUILDCACHE_PASSWORD") 50 | if (cacheUsername != null && cachePassword != null) { 51 | credentials { 52 | username = cacheUsername 53 | password = cachePassword 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test.http: -------------------------------------------------------------------------------- 1 | ### 2 | 3 | POST https://music.youtube.com/youtubei/v1/next 4 | Content-Type: application/json 5 | Referer: https://music.youtube.com/ 6 | 7 | { 8 | "videoId": "GfCy_1xKFdU", 9 | "playlistId": "RDAMVMGfCy_1xKFdU", 10 | "params": "8gEAmgMDCNgE", 11 | "context": { 12 | "client": { 13 | "clientName": "WEB_REMIX", 14 | "clientVersion": "1.20220725.01.00" 15 | } 16 | } 17 | } 18 | 19 | ### 20 | 21 | POST https://music.youtube.com/youtubei/v1/music/get_search_suggestions 22 | Content-Type: application/json 23 | Referer: https://music.youtube.com/ 24 | 25 | { 26 | "input": "Never Gonna Give You Up", 27 | "context": { 28 | "client": { 29 | "clientName": "WEB_REMIX", 30 | "clientVersion": "1.20220502.01.00" 31 | } 32 | } 33 | } 34 | 35 | ### 36 | 37 | POST https://music.youtube.com/youtubei/v1/next?key=AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30&prettyPrint=false 38 | origin: https://music.youtube.com 39 | Content-Type: application/json 40 | 41 | { 42 | "videoId": "Yx6GdcG5XdM", 43 | "playlistId": "RDAMVMYx6GdcG5XdM", 44 | "params": "8gEAmgMDCNgE", 45 | "context": { 46 | "client": { 47 | "hl": "de", 48 | "gl": "DE", 49 | "clientName": "WEB_REMIX", 50 | "clientVersion": "1.20230102.01.00" 51 | } 52 | } 53 | } 54 | 55 | ### 56 | 57 | 58 | --------------------------------------------------------------------------------