├── .gitattributes ├── .github └── workflows │ ├── build.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── io │ └── github │ └── shaksternano │ └── borgar │ └── app │ └── App.kt ├── build.gradle.kts ├── core ├── build.gradle.kts └── src │ ├── main │ ├── kotlin │ │ └── io │ │ │ └── github │ │ │ └── shaksternano │ │ │ └── borgar │ │ │ └── core │ │ │ ├── Core.kt │ │ │ ├── collect │ │ │ └── CollectionUtil.kt │ │ │ ├── data │ │ │ ├── DatabaseConnection.kt │ │ │ ├── TableUtil.kt │ │ │ └── repository │ │ │ │ ├── Repository.kt │ │ │ │ ├── SavedUrlRepository.kt │ │ │ │ └── TemplateRepository.kt │ │ │ ├── emoji │ │ │ └── EmojiUtil.kt │ │ │ ├── exception │ │ │ └── Exceptions.kt │ │ │ ├── graphics │ │ │ ├── Fonts.kt │ │ │ ├── GraphicsUtil.kt │ │ │ ├── OverlayData.kt │ │ │ └── drawable │ │ │ │ ├── Drawable.kt │ │ │ │ ├── HorizontalDrawable.kt │ │ │ │ ├── ImageDrawable.kt │ │ │ │ ├── OutlinedTextDrawable.kt │ │ │ │ ├── ParagraphDrawable.kt │ │ │ │ ├── SimpleTextDrawable.kt │ │ │ │ └── TextDrawable.kt │ │ │ ├── io │ │ │ ├── DataSource.kt │ │ │ ├── DelegatedByteReadChannel.kt │ │ │ ├── HttpByteReadChannel.kt │ │ │ ├── HttpClientUtil.kt │ │ │ ├── IOUtil.kt │ │ │ ├── IndexedInputStream.kt │ │ │ ├── LazyInitByteReadChannel.kt │ │ │ ├── ModifiableInputStream.kt │ │ │ ├── SuspendCloseable.kt │ │ │ └── UrlInfo.kt │ │ │ ├── logging │ │ │ └── InterceptLogger.kt │ │ │ ├── media │ │ │ ├── DualBufferedImage.kt │ │ │ ├── FrameInfo.kt │ │ │ ├── ImageProcessor.kt │ │ │ ├── ImageUtil.kt │ │ │ ├── MediaProcessing.kt │ │ │ ├── MediaProcessingConfig.kt │ │ │ ├── MediaReaderFactory.kt │ │ │ ├── MediaReaders.kt │ │ │ ├── MediaUtil.kt │ │ │ ├── MediaWriterFactory.kt │ │ │ ├── MediaWriters.kt │ │ │ ├── SimpleMediaProcessingConfig.kt │ │ │ ├── VideoFrame.kt │ │ │ ├── reader │ │ │ │ ├── BaseMediaReader.kt │ │ │ │ ├── ConstantFrameDurationMediaReader.kt │ │ │ │ ├── FFmpegAudioReader.kt │ │ │ │ ├── FFmpegImageReader.kt │ │ │ │ ├── FFmpegMediaReader.kt │ │ │ │ ├── GifReader.kt │ │ │ │ ├── JavaxImageReader.kt │ │ │ │ ├── LimitedDurationMediaReader.kt │ │ │ │ ├── MediaReader.kt │ │ │ │ ├── NoAudioReader.kt │ │ │ │ ├── PdfReader.kt │ │ │ │ ├── WebPImageReader.kt │ │ │ │ └── ZippedImageReader.kt │ │ │ ├── template │ │ │ │ ├── CustomTemplate.kt │ │ │ │ ├── ResourceTemplate.kt │ │ │ │ └── Template.kt │ │ │ └── writer │ │ │ │ ├── FFmpegVideoWriter.kt │ │ │ │ ├── GifWriter.kt │ │ │ │ ├── Image4jIcoWriter.kt │ │ │ │ ├── JavaxImageWriter.kt │ │ │ │ ├── MediaWriter.kt │ │ │ │ ├── NoAudioWriter.kt │ │ │ │ └── PreProcessingWriter.kt │ │ │ ├── task │ │ │ ├── ApiFilesTask.kt │ │ │ ├── AutoCropTask.kt │ │ │ ├── CaptionTask.kt │ │ │ ├── CatTask.kt │ │ │ ├── CropTask.kt │ │ │ ├── DemotivateTask.kt │ │ │ ├── DerpibooruTask.kt │ │ │ ├── DownloadTask.kt │ │ │ ├── FileTask.kt │ │ │ ├── FindCropTask.kt │ │ │ ├── FlipTask.kt │ │ │ ├── GifTask.kt │ │ │ ├── InvertColorsTask.kt │ │ │ ├── LiveReactionTask.kt │ │ │ ├── LoopTask.kt │ │ │ ├── MediaProcessingTask.kt │ │ │ ├── MemeTask.kt │ │ │ ├── PixelateTask.kt │ │ │ ├── ReduceFpsTask.kt │ │ │ ├── RotateTask.kt │ │ │ ├── SimpleMediaProcessingTask.kt │ │ │ ├── SpeechBubbleTask.kt │ │ │ ├── SpeedTask.kt │ │ │ ├── SpinTask.kt │ │ │ ├── StretchTask.kt │ │ │ ├── SubwaySurfersTask.kt │ │ │ ├── TemplateTask.kt │ │ │ ├── TranscodeTask.kt │ │ │ ├── UncaptionTask.kt │ │ │ └── UrlFileTask.kt │ │ │ └── util │ │ │ ├── AnyUtil.kt │ │ │ ├── ChannelEnvironment.kt │ │ │ ├── Displayed.kt │ │ │ ├── DurationUtil.kt │ │ │ ├── Environment.kt │ │ │ ├── FunctionUtil.kt │ │ │ ├── Identified.kt │ │ │ ├── JsonUtil.kt │ │ │ ├── MathUtil.kt │ │ │ ├── Named.kt │ │ │ ├── StringUtil.kt │ │ │ └── TenorUtil.kt │ └── resources │ │ ├── emoji │ │ ├── emoji_unicodes.txt │ │ └── emojis.json │ │ ├── font │ │ ├── bitstream_vera_sans.ttf │ │ ├── futura_condensed_extra_bold.otf │ │ ├── helvetica_neue.ttf │ │ ├── impact.ttf │ │ └── times.ttf │ │ ├── logback.xml │ │ ├── media │ │ ├── background │ │ │ └── live_reaction.png │ │ ├── containerimage │ │ │ ├── living_in_1984.png │ │ │ ├── oh_my_goodness_gracious.gif │ │ │ ├── sonic_says.png │ │ │ ├── soyjak_pointing.png │ │ │ ├── thinking_bubble.png │ │ │ ├── thinking_bubble_edge_trimmed.png │ │ │ ├── walmart_wanted.png │ │ │ └── who_did_this.png │ │ └── overlay │ │ │ ├── speech_bubble_1_full.png │ │ │ ├── speech_bubble_1_partial.png │ │ │ ├── speech_bubble_2_full.png │ │ │ ├── speech_bubble_2_partial.png │ │ │ └── subway_surfers_gameplay.mp4 │ │ └── shape │ │ └── thinking_bubble_edge_trimmed.javaobject │ └── test │ └── kotlin │ └── io │ └── github │ └── shaksternano │ └── borgar │ └── core │ ├── io │ ├── IndexedInputStreamTest.kt │ └── ModifiableInputStreamTest.kt │ ├── logging │ └── InterceptLoggerTest.kt │ └── task │ └── FileTaskTest.kt ├── discord ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── io │ └── github │ └── shaksternano │ └── borgar │ └── discord │ ├── Discord.kt │ ├── DiscordManager.kt │ ├── command │ ├── DiscordAutoCompleteHandler.kt │ ├── DiscordCommands.kt │ ├── DiscordOptionCommandArguments.kt │ └── DiscordSlashCommandHandler.kt │ ├── entity │ ├── DiscordCustomEmoji.kt │ ├── DiscordGroup.kt │ ├── DiscordGuild.kt │ ├── DiscordMember.kt │ ├── DiscordMentionable.kt │ ├── DiscordMessage.kt │ ├── DiscordPermissionHolder.kt │ ├── DiscordRole.kt │ ├── DiscordSticker.kt │ ├── DiscordUser.kt │ └── channel │ │ ├── DiscordChannel.kt │ │ └── DiscordMessageChannel.kt │ ├── event │ └── DiscordInteractionCommandEvent.kt │ ├── interaction │ ├── DiscordInteractionCommand.kt │ ├── DiscordInteractionCommandHandler.kt │ ├── message │ │ ├── CommandModalInteractionCommand.kt │ │ ├── DiscordMessageInteractionCommand.kt │ │ ├── DiscordMessageInteractionCommands.kt │ │ ├── DownloadInteractionCommand.kt │ │ ├── GifInteractionCommand.kt │ │ └── SelectMessageInteractionCommand.kt │ ├── modal │ │ ├── DiscordModalInteractionCommand.kt │ │ ├── DiscordModalInteractionCommands.kt │ │ └── RunCommandInteractionCommand.kt │ └── user │ │ ├── DiscordUserInteractionCommand.kt │ │ ├── DiscordUserInteractionCommands.kt │ │ ├── MemberAvatarInteractionCommand.kt │ │ ├── UserAvatarInteractionCommand.kt │ │ └── UserBannerInteractionCommand.kt │ ├── logging │ └── DiscordLogger.kt │ └── util │ └── DiscordPermissions.kt ├── docker-compose.yml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images └── image_caption_example.png ├── messaging ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── io │ │ └── github │ │ └── shaksternano │ │ └── borgar │ │ └── messaging │ │ ├── BotManager.kt │ │ ├── Messaging.kt │ │ ├── MessagingPlatform.kt │ │ ├── builder │ │ └── MessageBuilder.kt │ │ ├── command │ │ ├── ApiFilesCommand.kt │ │ ├── AutoCropCommand.kt │ │ ├── CaptionCommand.kt │ │ ├── CatCommand.kt │ │ ├── Command.kt │ │ ├── CommandArgumentInfo.kt │ │ ├── CommandArgumentType.kt │ │ ├── CommandArguments.kt │ │ ├── CommandAutoCompleteHandler.kt │ │ ├── CommandHandler.kt │ │ ├── CommandMessageIntersection.kt │ │ ├── CommandResponse.kt │ │ ├── Commands.kt │ │ ├── CreateTemplateCommand.kt │ │ ├── CropCommand.kt │ │ ├── CutoutSpeechBubbleCommand.kt │ │ ├── DeleteTemplateCommand.kt │ │ ├── DemotivateCommand.kt │ │ ├── DerpibooruCommand.kt │ │ ├── DownloadCommand.kt │ │ ├── EmojiImageCommand.kt │ │ ├── FavouriteCommand.kt │ │ ├── FileCommand.kt │ │ ├── FileExecutable.kt │ │ ├── FlipCommand.kt │ │ ├── GifCommand.kt │ │ ├── GuildBannerCommand.kt │ │ ├── GuildCountCommand.kt │ │ ├── GuildIconCommand.kt │ │ ├── GuildSplashCommand.kt │ │ ├── HelpCommand.kt │ │ ├── InvertColorsCommand.kt │ │ ├── LiveReactionCommand.kt │ │ ├── LoopCommand.kt │ │ ├── MemeCommand.kt │ │ ├── MessageCommandArguments.kt │ │ ├── Permission.kt │ │ ├── PingCommand.kt │ │ ├── PixelateCommand.kt │ │ ├── ReduceFpsCommand.kt │ │ ├── ResizeCommand.kt │ │ ├── ReverseCommand.kt │ │ ├── RotateCommand.kt │ │ ├── ShutdownCommand.kt │ │ ├── SpeechBubbleCommand.kt │ │ ├── SpeedCommand.kt │ │ ├── SpinCommand.kt │ │ ├── StickerImageCommand.kt │ │ ├── StretchCommand.kt │ │ ├── SubwaySurfersCommand.kt │ │ ├── TemplateCommand.kt │ │ ├── TenorUrlCommand.kt │ │ ├── TranscodeCommand.kt │ │ ├── UncaptionCommand.kt │ │ ├── UptimeCommand.kt │ │ ├── UrlFileCommand.kt │ │ ├── UserAvatarCommand.kt │ │ ├── UserBannerCommand.kt │ │ └── Validator.kt │ │ ├── entity │ │ ├── Attachment.kt │ │ ├── BaseEntity.kt │ │ ├── ChatRoom.kt │ │ ├── CustomEmoji.kt │ │ ├── DisplayedUser.kt │ │ ├── Entity.kt │ │ ├── FakeMessage.kt │ │ ├── Group.kt │ │ ├── Guild.kt │ │ ├── Managed.kt │ │ ├── Member.kt │ │ ├── Mentionable.kt │ │ ├── Message.kt │ │ ├── MessageEmbed.kt │ │ ├── PermissionHolder.kt │ │ ├── Role.kt │ │ ├── Sticker.kt │ │ ├── TimeStamped.kt │ │ ├── User.kt │ │ └── channel │ │ │ ├── Channel.kt │ │ │ └── MessageChannel.kt │ │ ├── event │ │ ├── CommandEvent.kt │ │ ├── Event.kt │ │ ├── MessageCommandEvent.kt │ │ └── MessageReceiveEvent.kt │ │ ├── exception │ │ └── Exceptions.kt │ │ └── util │ │ ├── FavouriteHandler.kt │ │ ├── MessageListener.kt │ │ ├── MessageUtil.kt │ │ └── SelectedMessages.kt │ └── test │ └── kotlin │ └── io │ └── github │ └── shaksternano │ └── borgar │ └── messaging │ └── command │ └── CommandParsingTest.kt ├── revolt ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── io │ └── github │ └── shaksternano │ └── borgar │ └── revolt │ ├── Revolt.kt │ ├── RevoltManager.kt │ ├── entity │ ├── RevoltCustomEmoji.kt │ ├── RevoltGroup.kt │ ├── RevoltGuild.kt │ ├── RevoltMember.kt │ ├── RevoltMessage.kt │ ├── RevoltRole.kt │ ├── RevoltUser.kt │ └── channel │ │ ├── RevoltChannel.kt │ │ ├── RevoltChannelType.kt │ │ └── RevoltMessageChannel.kt │ ├── util │ └── RevoltPermission.kt │ └── websocket │ ├── RevoltWebSocketClient.kt │ ├── WebSocketMessageHandler.kt │ └── WebSocketMessageType.kt ├── scripts ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── io │ └── github │ └── shaksternano │ └── borgar │ └── scripts │ ├── emoji │ ├── EmojiShortcodesFileGenerator.kt │ └── EmojiUnicodesFileGenerator.kt │ └── util │ └── GitHubUtil.kt └── settings.gradle.kts /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [ push, pull_request ] 3 | 4 | jobs: 5 | build: 6 | name: Build project 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout sources 10 | uses: actions/checkout@v4 11 | 12 | - name: Setup Java 13 | uses: actions/setup-java@v4 14 | with: 15 | distribution: "graalvm" 16 | java-version: 21 17 | 18 | - name: Setup Gradle 19 | uses: gradle/actions/setup-gradle@v4 20 | 21 | - name: Build 22 | run: ./gradlew build --refresh-dependencies --parallel 23 | 24 | - name: Upload build artifacts 25 | uses: actions/upload-artifact@v4 26 | with: 27 | name: Artifacts 28 | path: build/libs/ 29 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [ push, pull_request ] 3 | 4 | jobs: 5 | test: 6 | name: Run tests 7 | runs-on: ubuntu-latest 8 | env: 9 | POSTGRESQL_URL: jdbc:postgresql://localhost:5432/postgres 10 | POSTGRESQL_PORTS: 5432:5432 11 | POSTGRESQL_USERNAME: root 12 | POSTGRESQL_PASSWORD: password 13 | steps: 14 | - name: Checkout sources 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup Java 18 | uses: actions/setup-java@v4 19 | with: 20 | distribution: "graalvm" 21 | java-version: 21 22 | 23 | - name: Setup Gradle 24 | uses: gradle/actions/setup-gradle@v4 25 | 26 | - name: Create .env file for Docker Compose 27 | run: | 28 | touch .env 29 | echo POSTGRESQL_PORTS=POSTGRESQL_PORTS >> .env 30 | echo POSTGRESQL_USERNAME=$POSTGRESQL_USERNAME >> .env 31 | echo POSTGRESQL_PASSWORD=$POSTGRESQL_PASSWORD >> .env 32 | 33 | - name: Run Docker Compose 34 | run: docker compose up -d 35 | 36 | - name: Run tests 37 | run: ./gradlew cleanTest test --refresh-dependencies --stacktrace --parallel 38 | 39 | - name: Put all test reports into one directory 40 | run: | 41 | mkdir -p build/reports/tests/core 42 | mkdir -p build/reports/tests/messaging 43 | cp -r core/build/reports/tests/test/* build/reports/tests/core/ 44 | cp -r messaging/build/reports/tests/test/* build/reports/tests/messaging/ 45 | 46 | - name: Upload test report 47 | if: success() || failure() 48 | uses: actions/upload-artifact@v4 49 | with: 50 | name: Test report 51 | path: build/reports/tests/ 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Gradle ### 2 | .gradle/ 3 | build/ 4 | out/ 5 | classes/ 6 | 7 | ### Eclipse ### 8 | *.launch 9 | 10 | ### IntelliJ IDEA ### 11 | .idea/ 12 | *.iml 13 | *.ipr 14 | *.iws 15 | 16 | ### VS Code ### 17 | .settings/ 18 | .vscode/ 19 | bin/ 20 | .classpath 21 | .project 22 | 23 | ### macOS ### 24 | *.DS_Store 25 | 26 | ### Fleet ### 27 | .fleet/ 28 | 29 | ### Other ### 30 | .kotlin/ 31 | *.log 32 | .env 33 | *.mapdb 34 | *.mapdb.wal.0 35 | templates/ 36 | derpibooru_tags.txt 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 ShaksterNano 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 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.gradleup.shadow") version "8.3.6" 3 | application 4 | } 5 | 6 | base.archivesName.set("borgar") 7 | 8 | dependencies { 9 | api(project(":discord")) 10 | api(project(":revolt")) 11 | 12 | testImplementation(kotlin("test")) 13 | } 14 | 15 | val mainClassFullName = "${project.group}.borgar.app.AppKt" 16 | 17 | tasks { 18 | jar { 19 | enabled = false 20 | } 21 | 22 | shadowJar { 23 | archiveClassifier.set("") 24 | mergeServiceFiles() 25 | manifest { 26 | attributes( 27 | mapOf( 28 | "Main-Class" to mainClassFullName, 29 | ) 30 | ) 31 | } 32 | dependsOn(distTar, distZip) 33 | } 34 | 35 | val copyJar = register("copyJar") { 36 | from(layout.buildDirectory.file("libs/${base.archivesName.get()}-$version.jar")) 37 | into(rootProject.layout.buildDirectory.dir("libs")) 38 | } 39 | 40 | build { 41 | dependsOn(shadowJar) 42 | finalizedBy(copyJar) 43 | } 44 | } 45 | 46 | application { 47 | mainClass.set(mainClassFullName) 48 | tasks.run.get().workingDir = rootProject.projectDir 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/github/shaksternano/borgar/app/App.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.app 2 | 3 | import io.github.shaksternano.borgar.core.START_TIME 4 | import io.github.shaksternano.borgar.core.initCore 5 | import io.github.shaksternano.borgar.core.logger 6 | import io.github.shaksternano.borgar.core.util.getEnvVar 7 | import io.github.shaksternano.borgar.discord.initDiscord 8 | import io.github.shaksternano.borgar.messaging.initMessaging 9 | import io.github.shaksternano.borgar.revolt.initRevolt 10 | import kotlin.time.DurationUnit 11 | import kotlin.time.TimeSource 12 | 13 | private const val DISCORD_BOT_TOKEN_ENV_VAR: String = "DISCORD_BOT_TOKEN" 14 | private const val REVOLT_BOT_TOKEN_ENV_VAR: String = "REVOLT_BOT_TOKEN" 15 | 16 | suspend fun main() { 17 | logger.info("Starting") 18 | initCore() 19 | initMessaging() 20 | val discordToken = getEnvVar(DISCORD_BOT_TOKEN_ENV_VAR) 21 | val revoltToken = getEnvVar(REVOLT_BOT_TOKEN_ENV_VAR) 22 | if (discordToken == null && revoltToken == null) { 23 | logger.error("No bot tokens found") 24 | return 25 | } 26 | if (discordToken != null) { 27 | initDiscord(discordToken) 28 | } 29 | if (revoltToken != null) { 30 | initRevolt(revoltToken) 31 | } 32 | val time = TimeSource.Monotonic.markNow() 33 | val timeTaken = time - START_TIME 34 | val timeTakenString = timeTaken.toString(DurationUnit.SECONDS, 3) 35 | logger.info("Finished loading in $timeTakenString") 36 | } 37 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") 3 | kotlin("plugin.serialization") 4 | id("org.jetbrains.kotlinx.atomicfu") 5 | } 6 | 7 | allprojects { 8 | group = "io.github.shaksternano" 9 | version = "1.0.0" 10 | 11 | applyPlugins( 12 | "kotlin", 13 | "kotlinx-serialization", 14 | "org.jetbrains.kotlinx.atomicfu", 15 | ) 16 | 17 | repositories { 18 | mavenCentral() 19 | maven("https://central.sonatype.com/repository/maven-snapshots") 20 | maven("https://jitpack.io") 21 | } 22 | 23 | tasks { 24 | test { 25 | useJUnitPlatform() 26 | } 27 | } 28 | } 29 | 30 | fun PluginAware.applyPlugins(vararg plugins: String) { 31 | plugins.forEach { 32 | apply(plugin = it) 33 | } 34 | } 35 | 36 | tasks { 37 | jar { 38 | enabled = false 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/Core.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core 2 | 3 | import io.github.shaksternano.borgar.core.data.connectToDatabase 4 | import io.github.shaksternano.borgar.core.emoji.initEmojis 5 | import io.github.shaksternano.borgar.core.graphics.registerFonts 6 | import io.github.shaksternano.borgar.core.util.getEnvVar 7 | import io.github.shaksternano.borgar.core.util.loadEnv 8 | import org.bytedeco.ffmpeg.global.avutil 9 | import org.slf4j.Logger 10 | import org.slf4j.LoggerFactory 11 | import kotlin.io.path.Path 12 | import kotlin.time.TimeSource 13 | 14 | val START_TIME: TimeSource.Monotonic.ValueTimeMark = TimeSource.Monotonic.markNow() 15 | val AVAILABLE_PROCESSORS: Int = Runtime.getRuntime().availableProcessors() 16 | 17 | val baseLogger: Logger = LoggerFactory.getLogger("Borgar") 18 | var logger: Logger = baseLogger 19 | 20 | suspend fun initCore() { 21 | val envFileName = ".env" 22 | loadEnv(Path(envFileName)) 23 | connectToPostgreSql() 24 | registerFonts() 25 | initEmojis() 26 | avutil.av_log_set_level(avutil.AV_LOG_PANIC) 27 | } 28 | 29 | private fun connectToPostgreSql() { 30 | val url = getEnvVar("POSTGRESQL_URL") ?: run { 31 | logger.warn("POSTGRESQL_URL environment variable not found!") 32 | return 33 | } 34 | val username = getEnvVar("POSTGRESQL_USERNAME") ?: run { 35 | logger.warn("POSTGRESQL_USERNAME environment variable not found!") 36 | return 37 | } 38 | val password = getEnvVar("POSTGRESQL_PASSWORD") ?: run { 39 | logger.warn("POSTGRESQL_PASSWORD environment variable not found!") 40 | return 41 | } 42 | connectToDatabase( 43 | url, 44 | username, 45 | password, 46 | "org.postgresql.Driver", 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/collect/CollectionUtil.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.collect 2 | 3 | import com.google.common.cache.Cache 4 | import kotlinx.coroutines.async 5 | import kotlinx.coroutines.awaitAll 6 | import kotlinx.coroutines.coroutineScope 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.flow 9 | 10 | fun MutableCollection.addAll(vararg elements: T): Boolean = 11 | addAll(elements) 12 | 13 | suspend fun Iterable.parallelMap(transform: suspend (T) -> R): List = 14 | if (this is Collection && size <= 1) 15 | map { transform(it) } 16 | else coroutineScope { 17 | map { async { transform(it) } }.awaitAll() 18 | } 19 | 20 | suspend fun Iterable.parallelForEach(action: suspend (T) -> Unit) { 21 | if (this is Collection && size <= 1) 22 | forEach { action(it) } 23 | else 24 | parallelMap(action) 25 | } 26 | 27 | fun MutableMap.putAllKeys(keys: Iterable, value: V) = keys.forEach { 28 | put(it, value) 29 | } 30 | 31 | inline fun forEachNotNull( 32 | supplier: () -> T?, 33 | action: (T) -> Unit, 34 | ) { 35 | var value = supplier() 36 | while (value != null) { 37 | action(value) 38 | value = supplier() 39 | } 40 | } 41 | 42 | operator fun Flow.plus(other: Flow): Flow = flow { 43 | collect { 44 | emit(it) 45 | } 46 | other.collect { 47 | emit(it) 48 | } 49 | } 50 | 51 | inline fun Cache.getOrPut(key: K, defaultValue: () -> V): V = 52 | getIfPresent(key) 53 | ?: defaultValue().also { 54 | put(key, it) 55 | } 56 | 57 | fun Cache.getAndInvalidate(key: K): V? = 58 | getIfPresent(key).also { 59 | invalidate(key) 60 | } 61 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/data/DatabaseConnection.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.data 2 | 3 | import org.jetbrains.exposed.sql.Database 4 | 5 | private lateinit var connection: Database 6 | 7 | fun connectToDatabase(url: String, user: String, password: String, driver: String) { 8 | if (!::connection.isInitialized) { 9 | connection = Database.connect( 10 | url = url, 11 | user = user, 12 | driver = driver, 13 | password = password, 14 | ) 15 | } 16 | } 17 | 18 | fun databaseConnection(): Database = 19 | if (::connection.isInitialized) { 20 | connection 21 | } else { 22 | throw IllegalStateException("Database connection not initialized") 23 | } 24 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/data/TableUtil.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.data 2 | 3 | import org.jetbrains.exposed.dao.id.EntityID 4 | import org.jetbrains.exposed.dao.id.IdTable 5 | import org.jetbrains.exposed.sql.Column 6 | 7 | open class VarcharIdTable(name: String = "", columnName: String = "id", length: Int) : IdTable(name) { 8 | final override val id: Column> = varchar(columnName, length).entityId() 9 | final override val primaryKey = PrimaryKey(id) 10 | } 11 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/data/repository/Repository.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.data.repository 2 | 3 | import io.github.shaksternano.borgar.core.data.databaseConnection 4 | import io.github.shaksternano.borgar.core.io.IO_DISPATCHER 5 | import io.github.shaksternano.borgar.core.logger 6 | import org.jetbrains.exposed.sql.SchemaUtils 7 | import org.jetbrains.exposed.sql.Table 8 | import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction 9 | import org.jetbrains.exposed.sql.transactions.transaction 10 | 11 | abstract class Repository { 12 | 13 | init { 14 | try { 15 | transaction(databaseConnection()) { 16 | SchemaUtils.create(table()) 17 | } 18 | } catch (t: Throwable) { 19 | logger.error("Failed to create table", t) 20 | } 21 | } 22 | 23 | protected abstract fun table(): Table 24 | 25 | protected suspend fun dbQuery(block: suspend Table.() -> R): R = 26 | newSuspendedTransaction(IO_DISPATCHER) { block(table()) } 27 | } 28 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/data/repository/SavedUrlRepository.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.data.repository 2 | 3 | import io.github.shaksternano.borgar.core.data.VarcharIdTable 4 | import org.jetbrains.exposed.sql.insert 5 | import org.jetbrains.exposed.sql.selectAll 6 | 7 | object SavedUrlRepository : Repository() { 8 | 9 | override fun table(): SavedUrlTable = SavedUrlTable 10 | 11 | suspend fun createAlias(url: String, aliasUrl: String): Unit = dbQuery { 12 | insert { 13 | it[SavedUrlTable.url] = url 14 | it[SavedUrlTable.aliasUrl] = aliasUrl 15 | } 16 | } 17 | 18 | suspend fun readAliasUrl(url: String): String? = dbQuery { 19 | selectAll().where { SavedUrlTable.url eq url } 20 | .map { it[SavedUrlTable.aliasUrl] } 21 | .firstOrNull() 22 | } 23 | } 24 | 25 | object SavedUrlTable : VarcharIdTable(name = "saved_url", columnName = "url", length = 500) { 26 | val url = id 27 | val aliasUrl = varchar("alias_url", 500).uniqueIndex() 28 | } 29 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/exception/Exceptions.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.exception 2 | 3 | import io.ktor.http.* 4 | import kotlinx.io.IOException 5 | 6 | class ErrorResponseException( 7 | override val message: String, 8 | cause: Throwable? = null, 9 | ) : Exception(message, cause) 10 | 11 | class UnreadableFileException( 12 | override val cause: Throwable, 13 | ) : IOException(cause) 14 | 15 | class HttpException( 16 | override val message: String, 17 | val status: HttpStatusCode, 18 | ) : IOException(message) 19 | 20 | class FileTooLargeException( 21 | override val cause: Throwable? = null, 22 | ) : Exception(cause) 23 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/graphics/Fonts.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.graphics 2 | 3 | import io.github.shaksternano.borgar.core.io.IO_DISPATCHER 4 | import io.github.shaksternano.borgar.core.io.forEachResource 5 | import io.github.shaksternano.borgar.core.logger 6 | import kotlinx.coroutines.withContext 7 | import java.awt.Font 8 | import java.awt.GraphicsEnvironment 9 | import java.io.InputStream 10 | 11 | const val DEFAULT_FONT_NAME: String = Font.DIALOG 12 | 13 | suspend fun registerFonts() = forEachResource( 14 | "font" 15 | ) { resourcePath: String, inputStream: InputStream -> 16 | runCatching { 17 | val font = withContext(IO_DISPATCHER) { 18 | Font.createFont(Font.TRUETYPE_FONT, inputStream) 19 | } 20 | GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont(font) 21 | }.onFailure { 22 | logger.error("Error loading font $resourcePath", it) 23 | } 24 | } 25 | 26 | fun fontExists(fontName: String): Boolean = 27 | GraphicsEnvironment.getLocalGraphicsEnvironment().allFonts.any { 28 | it.name == fontName 29 | } 30 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/graphics/OverlayData.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.graphics 2 | 3 | data class OverlayData( 4 | val overlaidWidth: Int, 5 | val overlaidHeight: Int, 6 | val image1X: Int, 7 | val image1Y: Int, 8 | val image2X: Int, 9 | val image2Y: Int, 10 | val overlaidImageType: Int, 11 | ) 12 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/graphics/drawable/Drawable.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.graphics.drawable 2 | 3 | import io.github.shaksternano.borgar.core.graphics.Position 4 | import io.github.shaksternano.borgar.core.io.SuspendCloseable 5 | import java.awt.Graphics2D 6 | import kotlin.time.Duration 7 | 8 | interface Drawable : SuspendCloseable { 9 | 10 | /** 11 | * Draws the drawable. 12 | * 13 | * @param graphics The graphics object to draw on. 14 | * @param x The x coordinate of the top left corner of the drawable. 15 | * @param y The y coordinate of the top left corner of the drawable. 16 | * @param timestamp The timestamp of the frame. 17 | */ 18 | suspend fun draw(graphics: Graphics2D, x: Int, y: Int, timestamp: Duration) 19 | 20 | suspend fun getWidth(graphics: Graphics2D): Int 21 | 22 | suspend fun getHeight(graphics: Graphics2D): Int 23 | 24 | /** 25 | * Creates a new drawable from this one that has been resized to the given height. 26 | * 27 | * @param height The height to resize to. 28 | * @return A new drawable that has been resized to the given width, 29 | * or null if height resizing is not supported. 30 | */ 31 | fun resizeToHeight(height: Int): Drawable? 32 | 33 | override suspend fun close() = Unit 34 | } 35 | 36 | suspend fun Drawable.draw(graphics: Graphics2D, position: Position, timestamp: Duration) { 37 | draw(graphics, position.x, position.y, timestamp) 38 | } 39 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/graphics/drawable/HorizontalDrawable.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.graphics.drawable 2 | 3 | import io.github.shaksternano.borgar.core.io.closeAll 4 | import io.github.shaksternano.borgar.core.util.kClass 5 | import java.awt.Graphics2D 6 | import kotlin.math.max 7 | import kotlin.time.Duration 8 | 9 | class HorizontalDrawable( 10 | private val parts: Iterable, 11 | ) : Drawable { 12 | 13 | override suspend fun draw(graphics: Graphics2D, x: Int, y: Int, timestamp: Duration) { 14 | var drawableX = x 15 | parts.forEach { 16 | it.draw(graphics, drawableX, y, timestamp) 17 | drawableX += it.getWidth(graphics) 18 | } 19 | } 20 | 21 | override suspend fun getWidth(graphics: Graphics2D): Int = 22 | parts.fold(0) { width, part -> 23 | width + part.getWidth(graphics) 24 | } 25 | 26 | override suspend fun getHeight(graphics: Graphics2D): Int = 27 | parts.fold(0) { height, part -> 28 | max(height, part.getHeight(graphics)) 29 | } 30 | 31 | override fun resizeToHeight(height: Int): Drawable { 32 | var resizedAny = false 33 | val resizedParts = parts.map { 34 | it.resizeToHeight(height)?.also { 35 | resizedAny = true 36 | } ?: it 37 | } 38 | return if (resizedAny) { 39 | HorizontalDrawable(resizedParts) 40 | } else { 41 | this 42 | } 43 | } 44 | 45 | override suspend fun close() = 46 | closeAll(parts) 47 | 48 | override fun equals(other: Any?): Boolean { 49 | if (this === other) return true 50 | if (kClass != other?.kClass) return false 51 | 52 | other as HorizontalDrawable 53 | 54 | return parts == other.parts 55 | } 56 | 57 | override fun hashCode(): Int = 58 | parts.hashCode() 59 | 60 | override fun toString(): String { 61 | return "HorizontalCompositeDrawable(parts=$parts)" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/graphics/drawable/TextDrawable.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.graphics.drawable 2 | 3 | interface TextDrawable : Drawable { 4 | 5 | val text: String 6 | } 7 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/io/DelegatedByteReadChannel.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.io 2 | 3 | import io.ktor.utils.io.* 4 | import kotlinx.io.Buffer 5 | import kotlinx.io.Source 6 | 7 | abstract class DelegatedByteReadChannel : ByteReadChannel { 8 | 9 | private lateinit var delegate: ByteReadChannel 10 | private val isInitialized: Boolean 11 | get() = this::delegate.isInitialized 12 | 13 | override val closedCause: Throwable? 14 | get() = 15 | if (isInitialized) delegate.closedCause 16 | else null 17 | override val isClosedForRead: Boolean 18 | get() = 19 | if (isInitialized) delegate.isClosedForRead 20 | else false 21 | 22 | @InternalAPI 23 | override val readBuffer: Source 24 | get() = 25 | if (isInitialized) delegate.readBuffer 26 | else Buffer() 27 | 28 | protected abstract suspend fun createChannel(): ByteReadChannel 29 | 30 | private suspend fun init() { 31 | if (!isInitialized) delegate = createChannel() 32 | } 33 | 34 | override suspend fun awaitContent(min: Int): Boolean { 35 | init() 36 | return delegate.awaitContent(min) 37 | } 38 | 39 | override fun cancel(cause: Throwable?) { 40 | if (isInitialized) { 41 | delegate.cancel(cause) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/io/HttpByteReadChannel.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.io 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.statement.* 5 | import io.ktor.utils.io.* 6 | 7 | class HttpByteReadChannel( 8 | private val url: String, 9 | ) : DelegatedByteReadChannel() { 10 | 11 | private val httpClient: HttpClient = configuredHttpClient(json = false) 12 | 13 | override suspend fun createChannel(): ByteReadChannel { 14 | val response = httpClient.get(url) 15 | return response.bodyAsChannel() 16 | } 17 | 18 | override fun cancel(cause: Throwable?) { 19 | httpClient.close() 20 | return super.cancel(cause) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/io/IndexedInputStream.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.io 2 | 3 | import java.io.InputStream 4 | 5 | open class IndexedInputStream( 6 | private val inputStream: InputStream 7 | ) : InputStream() { 8 | 9 | var nextIndex = 0L 10 | private set 11 | private var markIndex = 0L 12 | 13 | override fun read(): Int { 14 | val byte = inputStream.read() 15 | if (byte != -1) { 16 | nextIndex++ 17 | } 18 | return byte 19 | } 20 | 21 | override fun skip(n: Long): Long { 22 | val skipped = inputStream.skip(n) 23 | nextIndex += skipped 24 | return skipped 25 | } 26 | 27 | override fun available(): Int { 28 | return inputStream.available() 29 | } 30 | 31 | override fun close() { 32 | inputStream.close() 33 | } 34 | 35 | override fun mark(readlimit: Int) { 36 | inputStream.mark(readlimit) 37 | markIndex = nextIndex 38 | } 39 | 40 | override fun reset() { 41 | inputStream.reset() 42 | nextIndex = markIndex 43 | } 44 | 45 | override fun markSupported(): Boolean { 46 | return inputStream.markSupported() 47 | } 48 | } 49 | 50 | fun InputStream.indexed(): IndexedInputStream = IndexedInputStream(this) 51 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/io/LazyInitByteReadChannel.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.io 2 | 3 | import io.ktor.utils.io.* 4 | 5 | class LazyInitByteReadChannel( 6 | private val provider: suspend () -> ByteReadChannel, 7 | ) : DelegatedByteReadChannel() { 8 | 9 | override suspend fun createChannel(): ByteReadChannel = provider() 10 | } 11 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/io/ModifiableInputStream.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.io 2 | 3 | import kotlinx.io.EOFException 4 | import java.io.InputStream 5 | 6 | class ModifiableInputStream( 7 | inputStream: InputStream 8 | ) : IndexedInputStream(inputStream) { 9 | 10 | private val toInsert: MutableMap> = mutableMapOf() 11 | private val inserting: MutableList = mutableListOf() 12 | private val toRemove: MutableMap = mutableMapOf() 13 | 14 | fun insertBytes(index: Long, bytes: List) { 15 | toInsert[index] = bytes 16 | } 17 | 18 | fun removeBytes(index: Long, length: Long) { 19 | toRemove[index] = length 20 | } 21 | 22 | override fun read(): Int { 23 | fillInserting() 24 | val inserted = inserting.removeFirstOrNull() 25 | if (inserted != null) { 26 | return inserted 27 | } 28 | val removedLength = toRemove.remove(nextIndex) 29 | if (removedLength != null) { 30 | try { 31 | skipNBytes(removedLength) 32 | } catch (e: EOFException) { 33 | return -1 34 | } 35 | } 36 | return super.read() 37 | } 38 | 39 | private fun fillInserting() { 40 | if (inserting.isEmpty()) { 41 | val newInserting = toInsert.remove(nextIndex) 42 | if (newInserting != null) { 43 | inserting.addAll(newInserting) 44 | } 45 | } 46 | } 47 | } 48 | 49 | fun InputStream.modifiable(): ModifiableInputStream = ModifiableInputStream(this) 50 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/io/UrlInfo.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.io 2 | 3 | data class UrlInfo( 4 | val url: String, 5 | val filename: String = filename(url), 6 | val gifv: Boolean = false, 7 | ) : DataSourceConvertable { 8 | 9 | override fun asDataSource(): UrlDataSource = DataSource.fromUrl(url, filename) 10 | } 11 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/media/FrameInfo.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.media 2 | 3 | import kotlin.time.Duration 4 | 5 | data class FrameInfo( 6 | val duration: Duration, 7 | val timestamp: Duration, 8 | ) 9 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/media/MediaProcessingConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.media 2 | 3 | import io.github.shaksternano.borgar.core.media.reader.AudioReader 4 | import io.github.shaksternano.borgar.core.media.reader.ImageReader 5 | 6 | interface MediaProcessingConfig { 7 | 8 | val outputName: String 9 | get() = "" 10 | val outputExtension: String 11 | get() = "" 12 | 13 | suspend fun transformImageReader(imageReader: ImageReader, outputFormat: String): ImageReader = imageReader 14 | 15 | suspend fun transformAudioReader(audioReader: AudioReader, outputFormat: String): AudioReader = audioReader 16 | 17 | fun transformOutputFormat(inputFormat: String): String = inputFormat 18 | 19 | infix fun then(after: MediaProcessingConfig): MediaProcessingConfig { 20 | return ChainedMediaProcessingConfig(this, after) 21 | } 22 | } 23 | 24 | private class ChainedMediaProcessingConfig( 25 | private val first: MediaProcessingConfig, 26 | private val second: MediaProcessingConfig, 27 | ) : MediaProcessingConfig { 28 | 29 | override val outputName: String = second.outputName.ifBlank { 30 | first.outputName 31 | } 32 | override val outputExtension: String = second.outputExtension.ifBlank { 33 | first.outputExtension 34 | } 35 | 36 | override suspend fun transformImageReader(imageReader: ImageReader, outputFormat: String): ImageReader { 37 | val firstReader = first.transformImageReader(imageReader, outputFormat) 38 | return second.transformImageReader(firstReader, outputFormat) 39 | } 40 | 41 | override suspend fun transformAudioReader(audioReader: AudioReader, outputFormat: String): AudioReader { 42 | val firstReader = first.transformAudioReader(audioReader, outputFormat) 43 | return second.transformAudioReader(firstReader, outputFormat) 44 | } 45 | 46 | override fun transformOutputFormat(inputFormat: String): String { 47 | val firstFormat = first.transformOutputFormat(inputFormat) 48 | return second.transformOutputFormat(firstFormat) 49 | } 50 | 51 | override fun toString(): String { 52 | return "ChainedMediaProcessingConfig(first=$first, second=$second, outputName='$outputName')" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/media/MediaReaderFactory.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.media 2 | 3 | import io.github.shaksternano.borgar.core.io.DataSource 4 | import io.github.shaksternano.borgar.core.media.reader.AudioReader 5 | import io.github.shaksternano.borgar.core.media.reader.ImageReader 6 | import io.github.shaksternano.borgar.core.media.reader.MediaReader 7 | 8 | interface MediaReaderFactory> { 9 | 10 | val supportedFormats: Set 11 | 12 | suspend fun create(input: DataSource): MediaReader 13 | } 14 | 15 | interface ImageReaderFactory : MediaReaderFactory { 16 | override suspend fun create(input: DataSource): ImageReader 17 | } 18 | 19 | interface AudioReaderFactory : MediaReaderFactory { 20 | override suspend fun create(input: DataSource): AudioReader 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/media/MediaWriterFactory.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.media 2 | 3 | import io.github.shaksternano.borgar.core.media.writer.MediaWriter 4 | import java.nio.file.Path 5 | import kotlin.time.Duration 6 | 7 | interface MediaWriterFactory { 8 | 9 | val supportedFormats: Set 10 | val maxImageDimension: Int 11 | get() = 0 12 | val requiredImageType: Int 13 | get() = 0 14 | 15 | suspend fun create( 16 | output: Path, 17 | outputFormat: String, 18 | loopCount: Int, 19 | audioChannels: Int, 20 | audioSampleRate: Int, 21 | audioBitrate: Int, 22 | maxFileSize: Long, 23 | maxDuration: Duration, 24 | ): MediaWriter 25 | } 26 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/media/MediaWriters.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.media 2 | 3 | import io.github.shaksternano.borgar.core.AVAILABLE_PROCESSORS 4 | import io.github.shaksternano.borgar.core.collect.putAllKeys 5 | import io.github.shaksternano.borgar.core.media.writer.* 6 | import io.github.shaksternano.borgar.core.util.then 7 | import java.awt.image.BufferedImage 8 | import java.nio.file.Path 9 | import kotlin.time.Duration 10 | 11 | private val writerFactories: Map = buildMap { 12 | registerFactory(JavaxImageWriter.Factory) 13 | registerFactory(GifWriter.Factory) 14 | registerFactory(Image4jIcoWriter.Factory) 15 | } 16 | 17 | private fun MutableMap.registerFactory( 18 | factory: MediaWriterFactory, 19 | ) = putAllKeys( 20 | factory.supportedFormats, 21 | factory, 22 | ) 23 | 24 | fun isWriterFormatSupported(format: String): Boolean = 25 | writerFactories.containsKey(format) 26 | 27 | suspend fun createWriter( 28 | output: Path, 29 | outputFormat: String, 30 | loopCount: Int, 31 | audioChannels: Int, 32 | audioSampleRate: Int, 33 | audioBitrate: Int, 34 | maxFileSize: Long, 35 | maxDuration: Duration, 36 | ): MediaWriter { 37 | val factory = writerFactories.getOrDefault(outputFormat, FFmpegVideoWriter.Factory) 38 | val writer = factory.create( 39 | output, 40 | outputFormat, 41 | loopCount, 42 | audioChannels, 43 | audioSampleRate, 44 | audioBitrate, 45 | maxFileSize, 46 | maxDuration, 47 | ) 48 | var preProcessing: ((BufferedImage) -> BufferedImage)? = null 49 | if (factory.maxImageDimension > 0) { 50 | preProcessing = { it.bound(factory.maxImageDimension) } 51 | } 52 | if (factory.requiredImageType > 0) { 53 | preProcessing = preProcessing.then { it.convertType(factory.requiredImageType) } 54 | } 55 | return if (preProcessing == null) { 56 | writer 57 | } else { 58 | PreProcessingWriter( 59 | writer = writer, 60 | maxConcurrency = AVAILABLE_PROCESSORS, 61 | preProcessImage = preProcessing, 62 | ) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/media/SimpleMediaProcessingConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.media 2 | 3 | import io.github.shaksternano.borgar.core.media.reader.ImageReader 4 | import io.github.shaksternano.borgar.core.media.reader.transform 5 | 6 | open class SimpleMediaProcessingConfig( 7 | val processor: ImageProcessor<*>, 8 | override val outputName: String = "", 9 | ) : MediaProcessingConfig { 10 | 11 | override suspend fun transformImageReader(imageReader: ImageReader, outputFormat: String): ImageReader { 12 | return imageReader.transform(processor, outputFormat) 13 | } 14 | 15 | override fun toString(): String { 16 | return "SimpleMediaProcessingConfig(processor=$processor, outputName='$outputName')" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/media/VideoFrame.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.media 2 | 3 | import org.bytedeco.javacv.Frame 4 | import java.awt.image.BufferedImage 5 | import kotlin.time.Duration 6 | 7 | data class VideoFrame( 8 | val content: T, 9 | val duration: Duration, 10 | val timestamp: Duration, 11 | ) 12 | 13 | typealias ImageFrame = VideoFrame 14 | typealias AudioFrame = VideoFrame 15 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/media/reader/JavaxImageReader.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.media.reader 2 | 3 | import io.github.shaksternano.borgar.core.io.DataSource 4 | import io.github.shaksternano.borgar.core.io.IO_DISPATCHER 5 | import io.github.shaksternano.borgar.core.media.ImageFrame 6 | import io.github.shaksternano.borgar.core.media.ImageReaderFactory 7 | import io.github.shaksternano.borgar.core.media.addTransparency 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlinx.coroutines.flow.flowOf 10 | import kotlinx.coroutines.withContext 11 | import java.awt.image.BufferedImage 12 | import javax.imageio.ImageIO 13 | import kotlin.time.Duration 14 | import kotlin.time.Duration.Companion.milliseconds 15 | 16 | class JavaxImageReader( 17 | image: BufferedImage, 18 | ) : BaseImageReader() { 19 | 20 | private val frame: ImageFrame = run { 21 | // For some reason some images have a greyscale type, even though they have color 22 | val converted = image.addTransparency() 23 | ImageFrame(converted, 1.milliseconds, Duration.ZERO) 24 | } 25 | 26 | override val frameCount: Int = 1 27 | override val frameRate: Double = 1.0 28 | override val duration: Duration = 1.milliseconds 29 | override val frameDuration: Duration = 1.milliseconds 30 | override val width: Int = frame.content.width 31 | override val height: Int = frame.content.height 32 | override val loopCount: Int = 0 33 | 34 | override suspend fun readFrame(timestamp: Duration): ImageFrame = frame 35 | 36 | override fun asFlow(): Flow = flowOf(frame) 37 | 38 | override suspend fun reversed(): MediaReader = this 39 | 40 | override suspend fun createChangedSpeed(speedMultiplier: Double): MediaReader = this 41 | 42 | override suspend fun close() = Unit 43 | 44 | object Factory : ImageReaderFactory { 45 | override val supportedFormats: Set = setOf( 46 | "bmp", 47 | "jpeg", 48 | "jpg", 49 | "wbmp", 50 | "png", 51 | "gif", 52 | "tif", 53 | "tiff", 54 | ) 55 | 56 | override suspend fun create(input: DataSource): ImageReader = input.newStream().use { 57 | val image = withContext(IO_DISPATCHER) { 58 | ImageIO.read(it) 59 | } ?: throw IllegalArgumentException("Failed to read image") 60 | JavaxImageReader(image) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/media/reader/NoAudioReader.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.media.reader 2 | 3 | import io.github.shaksternano.borgar.core.io.DataSource 4 | import io.github.shaksternano.borgar.core.media.AudioFrame 5 | import io.github.shaksternano.borgar.core.media.AudioReaderFactory 6 | import kotlinx.coroutines.flow.Flow 7 | import kotlinx.coroutines.flow.emptyFlow 8 | import kotlin.time.Duration 9 | 10 | object NoAudioReader : BaseAudioReader() { 11 | 12 | override val frameCount: Int = 0 13 | override val frameRate: Double = 0.0 14 | override val duration: Duration = Duration.ZERO 15 | override val frameDuration: Duration = Duration.ZERO 16 | override val audioChannels: Int = 0 17 | override val audioSampleRate: Int = 0 18 | override val audioBitrate: Int = 0 19 | override val loopCount: Int = 0 20 | 21 | override suspend fun readFrame(timestamp: Duration): AudioFrame = 22 | throw UnsupportedOperationException("No audio available") 23 | 24 | override fun asFlow(): Flow = emptyFlow() 25 | 26 | override suspend fun reversed(): MediaReader = this 27 | 28 | override suspend fun createChangedSpeed(speedMultiplier: Double): MediaReader = this 29 | 30 | override suspend fun close() = Unit 31 | 32 | class Factory( 33 | override val supportedFormats: Set 34 | ) : AudioReaderFactory { 35 | override suspend fun create(input: DataSource): AudioReader = NoAudioReader 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/media/template/Template.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.media.template 2 | 3 | import io.github.shaksternano.borgar.core.graphics.ContentPosition 4 | import io.github.shaksternano.borgar.core.graphics.TextAlignment 5 | import io.github.shaksternano.borgar.core.graphics.drawable.Drawable 6 | import io.github.shaksternano.borgar.core.io.DataSource 7 | import io.github.shaksternano.borgar.core.media.reader.AudioReader 8 | import io.github.shaksternano.borgar.core.media.reader.ImageReader 9 | import java.awt.Color 10 | import java.awt.Font 11 | import java.awt.Shape 12 | 13 | interface Template { 14 | 15 | val media: DataSource 16 | val format: String 17 | val resultName: String 18 | 19 | val imageContentX: Int 20 | val imageContentY: Int 21 | val imageContentWidth: Int 22 | val imageContentHeight: Int 23 | val imageContentPosition: ContentPosition 24 | 25 | val textContentX: Int 26 | val textContentY: Int 27 | val textContentWidth: Int 28 | val textContentHeight: Int 29 | val textContentPosition: ContentPosition 30 | val textContentAlignment: TextAlignment 31 | 32 | val font: Font 33 | val textColor: Color 34 | val customTextDrawableSupplier: ((String) -> Drawable)? 35 | val contentRotationRadians: Double 36 | val isBackground: Boolean 37 | val fill: Color? 38 | val forceTransparency: Boolean 39 | 40 | suspend fun getImageReader(): ImageReader 41 | 42 | suspend fun getAudioReader(): AudioReader 43 | 44 | suspend fun getContentClip(): Shape? 45 | } 46 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/media/writer/GifWriter.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.media.writer 2 | 3 | import com.shakster.gifkt.GifEncoder 4 | import com.shakster.gifkt.ParallelGifEncoder 5 | import io.github.shaksternano.borgar.core.AVAILABLE_PROCESSORS 6 | import io.github.shaksternano.borgar.core.io.IO_DISPATCHER 7 | import io.github.shaksternano.borgar.core.media.ImageFrame 8 | import io.github.shaksternano.borgar.core.media.MediaWriterFactory 9 | import kotlinx.coroutines.withContext 10 | import java.nio.file.Path 11 | import kotlin.time.Duration 12 | 13 | class GifWriter( 14 | private val encoder: ParallelGifEncoder, 15 | ) : NoAudioWriter() { 16 | 17 | override val isStatic: Boolean = false 18 | 19 | override suspend fun writeImageFrame(frame: ImageFrame) { 20 | encoder.writeFrame(frame.content, frame.duration) 21 | } 22 | 23 | override suspend fun close() { 24 | encoder.close() 25 | } 26 | 27 | object Factory : MediaWriterFactory { 28 | 29 | override val supportedFormats: Set = setOf("gif") 30 | 31 | /** 32 | * 480p 33 | */ 34 | override val maxImageDimension: Int = 854 35 | 36 | override suspend fun create( 37 | output: Path, 38 | outputFormat: String, 39 | loopCount: Int, 40 | audioChannels: Int, 41 | audioSampleRate: Int, 42 | audioBitrate: Int, 43 | maxFileSize: Long, 44 | maxDuration: Duration, 45 | ): MediaWriter { 46 | val encoder = withContext(IO_DISPATCHER) { 47 | GifEncoder.builder(output) 48 | }.apply { 49 | transparencyColorTolerance = 0.01 50 | quantizedTransparencyColorTolerance = 0.02 51 | this.loopCount = loopCount 52 | comment = "GIF created with https://github.com/shaksternano/borgar" 53 | maxConcurrency = AVAILABLE_PROCESSORS 54 | ioContext = IO_DISPATCHER 55 | }.buildParallel() 56 | return GifWriter(encoder) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/media/writer/Image4jIcoWriter.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.media.writer 2 | 3 | import io.github.shaksternano.borgar.core.io.IO_DISPATCHER 4 | import io.github.shaksternano.borgar.core.media.ImageFrame 5 | import io.github.shaksternano.borgar.core.media.MediaWriterFactory 6 | import io.github.shaksternano.borgar.core.media.bound 7 | import kotlinx.coroutines.withContext 8 | import net.ifok.image.image4j.codec.ico.ICOEncoder 9 | import java.nio.file.Path 10 | import kotlin.time.Duration 11 | 12 | private const val MAX_DIMENSION: Int = 256 13 | 14 | class Image4jIcoWriter( 15 | private val output: Path, 16 | ) : NoAudioWriter() { 17 | 18 | override val isStatic: Boolean = true 19 | private var written: Boolean = false 20 | 21 | override suspend fun writeImageFrame(frame: ImageFrame) { 22 | if (written) return 23 | written = true 24 | val image = frame.content.bound(MAX_DIMENSION) 25 | withContext(IO_DISPATCHER) { 26 | ICOEncoder.write(image, output.toFile()) 27 | } 28 | } 29 | 30 | override suspend fun close() = Unit 31 | 32 | object Factory : MediaWriterFactory { 33 | override val supportedFormats: Set = setOf( 34 | "ico", 35 | ) 36 | 37 | override suspend fun create( 38 | output: Path, 39 | outputFormat: String, 40 | loopCount: Int, 41 | audioChannels: Int, 42 | audioSampleRate: Int, 43 | audioBitrate: Int, 44 | maxFileSize: Long, 45 | maxDuration: Duration 46 | ): MediaWriter = Image4jIcoWriter(output) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/media/writer/JavaxImageWriter.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.media.writer 2 | 3 | import io.github.shaksternano.borgar.core.io.IO_DISPATCHER 4 | import io.github.shaksternano.borgar.core.media.ImageFrame 5 | import io.github.shaksternano.borgar.core.media.MediaWriterFactory 6 | import io.github.shaksternano.borgar.core.media.convertType 7 | import io.github.shaksternano.borgar.core.media.supportsTransparency 8 | import kotlinx.coroutines.withContext 9 | import java.awt.image.BufferedImage 10 | import java.nio.file.Path 11 | import javax.imageio.ImageIO 12 | import kotlin.time.Duration 13 | 14 | class JavaxImageWriter( 15 | private val output: Path, 16 | private val outputFormat: String, 17 | ) : NoAudioWriter() { 18 | 19 | override val isStatic: Boolean = true 20 | private var written = false 21 | 22 | override suspend fun writeImageFrame(frame: ImageFrame) { 23 | if (written) return 24 | written = true 25 | val imageType = if (supportsTransparency(outputFormat)) { 26 | BufferedImage.TYPE_INT_ARGB 27 | } else { 28 | BufferedImage.TYPE_3BYTE_BGR 29 | } 30 | val image = frame.content.convertType(imageType) 31 | val supportedFormat = withContext(IO_DISPATCHER) { 32 | ImageIO.write(image, outputFormat, output.toFile()) 33 | } 34 | require(supportedFormat) { "Unsupported image format: $outputFormat" } 35 | } 36 | 37 | override suspend fun close() = Unit 38 | 39 | object Factory : MediaWriterFactory { 40 | override val supportedFormats: Set = setOf( 41 | "bmp", 42 | "jpeg", 43 | "jpg", 44 | "wbmp", 45 | "png", 46 | "gif", 47 | "tif", 48 | "tiff", 49 | ) 50 | 51 | override suspend fun create( 52 | output: Path, 53 | outputFormat: String, 54 | loopCount: Int, 55 | audioChannels: Int, 56 | audioSampleRate: Int, 57 | audioBitrate: Int, 58 | maxFileSize: Long, 59 | maxDuration: Duration 60 | ): MediaWriter = JavaxImageWriter(output, outputFormat) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/media/writer/MediaWriter.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.media.writer 2 | 3 | import io.github.shaksternano.borgar.core.io.SuspendCloseable 4 | import io.github.shaksternano.borgar.core.media.AudioFrame 5 | import io.github.shaksternano.borgar.core.media.ImageFrame 6 | 7 | interface MediaWriter : SuspendCloseable { 8 | 9 | val isStatic: Boolean 10 | val supportsAudio: Boolean 11 | 12 | suspend fun writeImageFrame(frame: ImageFrame) 13 | 14 | suspend fun writeAudioFrame(frame: AudioFrame) 15 | } 16 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/media/writer/NoAudioWriter.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.media.writer 2 | 3 | import io.github.shaksternano.borgar.core.media.AudioFrame 4 | 5 | abstract class NoAudioWriter : MediaWriter { 6 | 7 | final override val supportsAudio: Boolean = false 8 | 9 | final override suspend fun writeAudioFrame(frame: AudioFrame) = Unit 10 | } 11 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/task/CatTask.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.task 2 | 3 | import io.ktor.client.call.* 4 | import io.ktor.client.statement.* 5 | import kotlinx.serialization.Serializable 6 | import kotlin.random.Random 7 | 8 | private const val CAT_API_URL: String = "https://cataas.com" 9 | private const val GIF_CHANCE: Double = 0.2 10 | 11 | class CatTask( 12 | tags: String, 13 | fileCount: Int, 14 | maxFileSize: Long, 15 | ) : ApiFilesTask( 16 | tags, 17 | fileCount, 18 | filePrefix = "cat", 19 | maxFileSize, 20 | ) { 21 | 22 | override fun getRequestUrl(tags: Set): String { 23 | val isGif = 24 | if (tags.equalsAnyIgnoreCase("gif")) true 25 | else if (tags.equalsAnyIgnoreCase("image")) false 26 | else Random.nextDouble() < GIF_CHANCE 27 | val path = if (isGif) "/cat/gif" else "/cat" 28 | return "$CAT_API_URL$path?json=true" 29 | } 30 | 31 | private fun Iterable.equalsAnyIgnoreCase(string: String): Boolean = 32 | any { it.equals(string, ignoreCase = true) } 33 | 34 | override suspend fun parseResponse(response: HttpResponse): ApiResponse { 35 | val body = response.body() 36 | val extension = body.mimetype.split('/', limit = 2)[1] 37 | return ApiResponse( 38 | body.id, 39 | body.url, 40 | extension, 41 | ) 42 | } 43 | 44 | @Serializable 45 | private data class ResponseBody( 46 | val id: String, 47 | val url: String, 48 | val mimetype: String, 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/task/CropTask.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.task 2 | 3 | import io.github.shaksternano.borgar.core.media.ImageFrame 4 | import io.github.shaksternano.borgar.core.media.ImageProcessor 5 | import io.github.shaksternano.borgar.core.media.MediaProcessingConfig 6 | import io.github.shaksternano.borgar.core.media.SimpleMediaProcessingConfig 7 | import kotlinx.coroutines.flow.Flow 8 | import java.awt.image.BufferedImage 9 | import kotlin.math.max 10 | import kotlin.math.min 11 | 12 | class CropTask( 13 | topRatio: Double, 14 | bottomRatio: Double, 15 | leftRatio: Double, 16 | rightRatio: Double, 17 | maxFileSize: Long, 18 | ) : MediaProcessingTask(maxFileSize) { 19 | 20 | override val config: MediaProcessingConfig = SimpleMediaProcessingConfig( 21 | processor = CropProcessor( 22 | leftRatio, 23 | topRatio, 24 | rightRatio, 25 | bottomRatio, 26 | ), 27 | ) 28 | } 29 | 30 | private class CropProcessor( 31 | private val leftRatio: Double, 32 | private val topRatio: Double, 33 | private val rightRatio: Double, 34 | private val bottomRatio: Double, 35 | ) : ImageProcessor { 36 | 37 | override suspend fun constantData( 38 | firstFrame: ImageFrame, 39 | imageSource: Flow, 40 | outputFormat: String 41 | ): CropData { 42 | val firstImage = firstFrame.content 43 | val width = firstImage.width 44 | val height = firstImage.height 45 | val x = min(width * leftRatio, width - 1.0).toInt() 46 | val y = min(height * topRatio, height - 1.0).toInt() 47 | val newWidth = max(width * (1 - leftRatio - rightRatio), 1.0).toInt() 48 | val newHeight = max(height * (1 - topRatio - bottomRatio), 1.0).toInt() 49 | return CropData(x, y, newWidth, newHeight) 50 | } 51 | 52 | override suspend fun transformImage(frame: ImageFrame, constantData: CropData): BufferedImage = 53 | frame.content.getSubimage(constantData.x, constantData.y, constantData.width, constantData.height) 54 | } 55 | 56 | private class CropData( 57 | val x: Int, 58 | val y: Int, 59 | val width: Int, 60 | val height: Int, 61 | ) 62 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/task/FlipTask.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.task 2 | 3 | import io.github.shaksternano.borgar.core.media.ImageFrame 4 | import io.github.shaksternano.borgar.core.media.flipX 5 | import io.github.shaksternano.borgar.core.media.flipY 6 | import java.awt.image.BufferedImage 7 | 8 | class FlipTask( 9 | private val vertical: Boolean, 10 | maxFileSize: Long, 11 | ) : SimpleMediaProcessingTask(maxFileSize) { 12 | 13 | override suspend fun transformImage(frame: ImageFrame): BufferedImage { 14 | val image = frame.content 15 | return if (vertical) { 16 | image.flipY() 17 | } else { 18 | image.flipX() 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/task/GifTask.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.task 2 | 3 | import io.github.shaksternano.borgar.core.io.DataSource 4 | import io.github.shaksternano.borgar.core.io.fileExtension 5 | import io.github.shaksternano.borgar.core.io.filenameWithoutExtension 6 | import io.github.shaksternano.borgar.core.media.MediaProcessingConfig 7 | import io.github.shaksternano.borgar.core.media.isStaticOnly 8 | import io.github.shaksternano.borgar.core.media.processMedia 9 | 10 | class GifTask( 11 | private val forceTranscode: Boolean, 12 | private val forceRename: Boolean, 13 | maxFileSize: Long, 14 | ) : MediaProcessingTask(maxFileSize) { 15 | 16 | override val config: MediaProcessingConfig = GifConfig( 17 | forceTranscode = forceTranscode, 18 | forceRename = forceRename, 19 | ) 20 | 21 | override suspend fun process(input: DataSource): DataSource { 22 | return if (forceTranscode || !(forceRename || isStaticOnly(input.fileExtension))) { 23 | val config = TranscodeConfig("gif") 24 | processMedia(input, config, maxFileSize) 25 | } else { 26 | input.rename(input.filenameWithoutExtension + ".gif") 27 | } 28 | } 29 | } 30 | 31 | private class GifConfig( 32 | private val forceTranscode: Boolean, 33 | private val forceRename: Boolean, 34 | ) : MediaProcessingConfig { 35 | 36 | override val outputExtension: String = "gif" 37 | 38 | override fun transformOutputFormat(inputFormat: String): String { 39 | return if (forceTranscode || !(forceRename || isStaticOnly(inputFormat))) { 40 | "gif" 41 | } else { 42 | inputFormat 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/task/InvertColorsTask.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.task 2 | 3 | import io.github.shaksternano.borgar.core.media.ImageFrame 4 | import io.github.shaksternano.borgar.core.media.mapPixels 5 | import java.awt.image.BufferedImage 6 | 7 | class InvertColorsTask( 8 | maxFileSize: Long, 9 | ) : SimpleMediaProcessingTask(maxFileSize) { 10 | 11 | override suspend fun transformImage(frame: ImageFrame): BufferedImage { 12 | return frame.content.mapPixels { rgb -> 13 | val red = rgb shr 16 and 0xFF 14 | val green = rgb shr 8 and 0xFF 15 | val blue = rgb and 0xFF 16 | val alpha = rgb ushr 24 17 | val invertedRed = 255 - red 18 | val invertedGreen = 255 - green 19 | val invertedBlue = 255 - blue 20 | alpha shl 24 or (invertedRed shl 16) or (invertedGreen shl 8) or invertedBlue 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/task/MediaProcessingTask.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.task 2 | 3 | import io.github.shaksternano.borgar.core.io.DataSource 4 | import io.github.shaksternano.borgar.core.media.MediaProcessingConfig 5 | import io.github.shaksternano.borgar.core.media.processMedia 6 | 7 | abstract class MediaProcessingTask( 8 | protected val maxFileSize: Long, 9 | ) : MappedFileTask() { 10 | 11 | abstract val config: MediaProcessingConfig 12 | 13 | override suspend fun process(input: DataSource): DataSource = 14 | processMedia(input, config, maxFileSize) 15 | 16 | override fun then(after: FileTask): FileTask { 17 | if (!after.requireInput) { 18 | throw UnsupportedOperationException("The task after this one must require input") 19 | } 20 | return if (after is MediaProcessingTask) { 21 | ChainedMediaProcessingTask(this, after, maxFileSize) 22 | } else { 23 | super.then(after) 24 | } 25 | } 26 | } 27 | 28 | class ChainedMediaProcessingTask( 29 | val first: MediaProcessingTask, 30 | val second: MediaProcessingTask, 31 | maxFileSize: Long, 32 | ) : MediaProcessingTask(maxFileSize) { 33 | 34 | override val config: MediaProcessingConfig = first.config then second.config 35 | 36 | override fun toString(): String { 37 | return "ChainedMediaProcessingTask(maxFileSize=$maxFileSize, config=$config)" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/task/PixelateTask.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.task 2 | 3 | import io.github.shaksternano.borgar.core.media.ImageFrame 4 | import io.github.shaksternano.borgar.core.media.stretch 5 | import java.awt.image.BufferedImage 6 | 7 | class PixelateTask( 8 | private val pixelationMultiplier: Double, 9 | maxFileSize: Long, 10 | ) : SimpleMediaProcessingTask(maxFileSize) { 11 | 12 | override suspend fun transformImage(frame: ImageFrame): BufferedImage { 13 | val image = frame.content 14 | return image.stretch( 15 | (image.width / pixelationMultiplier).toInt(), 16 | (image.height / pixelationMultiplier).toInt(), 17 | true, 18 | ).stretch( 19 | image.width, 20 | image.height, 21 | true, 22 | ) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/task/ReduceFpsTask.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.task 2 | 3 | import io.github.shaksternano.borgar.core.media.MediaProcessingConfig 4 | import io.github.shaksternano.borgar.core.media.reader.ConstantFrameDurationMediaReader 5 | import io.github.shaksternano.borgar.core.media.reader.ImageReader 6 | 7 | class ReduceFpsTask( 8 | fpsReductionRatio: Double, 9 | maxFileSize: Long, 10 | ) : MediaProcessingTask(maxFileSize) { 11 | 12 | override val config: MediaProcessingConfig = ReduceFpsConfig(fpsReductionRatio) 13 | } 14 | 15 | private class ReduceFpsConfig( 16 | private val fpsReductionRatio: Double, 17 | ) : MediaProcessingConfig { 18 | 19 | override suspend fun transformImageReader(imageReader: ImageReader, outputFormat: String): ImageReader { 20 | val frameDuration = imageReader.frameDuration * fpsReductionRatio 21 | return ConstantFrameDurationMediaReader(imageReader, frameDuration) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/task/RotateTask.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.task 2 | 3 | import io.github.shaksternano.borgar.core.media.* 4 | import kotlinx.coroutines.flow.Flow 5 | import java.awt.Color 6 | import java.awt.image.BufferedImage 7 | 8 | class RotateTask( 9 | degrees: Double, 10 | backgroundColor: Color?, 11 | maxFileSize: Long, 12 | ) : MediaProcessingTask(maxFileSize) { 13 | 14 | override val config: MediaProcessingConfig = RotateConfig(degrees, backgroundColor) 15 | } 16 | 17 | private class RotateConfig( 18 | degrees: Double, 19 | backgroundColor: Color?, 20 | ) : SimpleMediaProcessingConfig( 21 | processor = RotateProcessor(degrees, backgroundColor), 22 | ) { 23 | 24 | override fun transformOutputFormat(inputFormat: String): String = 25 | equivalentTransparentFormat(inputFormat) 26 | } 27 | 28 | private class RotateProcessor( 29 | private val degrees: Double, 30 | private val backgroundColor: Color?, 31 | ) : ImageProcessor { 32 | 33 | override suspend fun constantData( 34 | firstFrame: ImageFrame, 35 | imageSource: Flow, 36 | outputFormat: String 37 | ): RotateData { 38 | val firstImage = firstFrame.content 39 | val resultType = firstImage.supportedTransparentImageType(outputFormat) 40 | return RotateData(resultType) 41 | } 42 | 43 | override suspend fun transformImage(frame: ImageFrame, constantData: RotateData): BufferedImage { 44 | val image = frame.content 45 | val radians = Math.toRadians(degrees) 46 | val resultType = constantData.resultImageType 47 | return image.rotate(radians, resultType, backgroundColor) 48 | } 49 | } 50 | 51 | @JvmInline 52 | private value class RotateData( 53 | val resultImageType: Int, 54 | ) 55 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/task/SimpleMediaProcessingTask.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.task 2 | 3 | import io.github.shaksternano.borgar.core.media.ImageFrame 4 | import io.github.shaksternano.borgar.core.media.ImageProcessor 5 | import io.github.shaksternano.borgar.core.media.MediaProcessingConfig 6 | import io.github.shaksternano.borgar.core.media.SimpleMediaProcessingConfig 7 | import kotlinx.coroutines.flow.Flow 8 | import java.awt.image.BufferedImage 9 | 10 | abstract class SimpleMediaProcessingTask( 11 | maxFileSize: Long, 12 | ) : MediaProcessingTask(maxFileSize) { 13 | 14 | final override val config: MediaProcessingConfig = SimpleMediaProcessingConfig( 15 | SimpleImageProcessor(::transformImage), 16 | ) 17 | 18 | abstract suspend fun transformImage(frame: ImageFrame): BufferedImage 19 | } 20 | 21 | private class SimpleImageProcessor( 22 | private val transform: suspend (ImageFrame) -> BufferedImage, 23 | ) : ImageProcessor { 24 | 25 | override suspend fun constantData(firstFrame: ImageFrame, imageSource: Flow, outputFormat: String) = 26 | Unit 27 | 28 | override suspend fun transformImage(frame: ImageFrame, constantData: Unit): BufferedImage = 29 | transform(frame) 30 | 31 | override fun toString(): String { 32 | return "SimpleImageProcessor(transform=$transform)" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/task/SpeedTask.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.task 2 | 3 | import io.github.shaksternano.borgar.core.media.MediaProcessingConfig 4 | import io.github.shaksternano.borgar.core.media.reader.AudioReader 5 | import io.github.shaksternano.borgar.core.media.reader.ImageReader 6 | 7 | class SpeedTask( 8 | speed: Double, 9 | maxFileSize: Long, 10 | outputName: String = "", 11 | ) : MediaProcessingTask(maxFileSize) { 12 | 13 | override val config: MediaProcessingConfig = SpeedConfig(speed, outputName) 14 | } 15 | 16 | private class SpeedConfig( 17 | private val speed: Double, 18 | override val outputName: String 19 | ) : MediaProcessingConfig { 20 | 21 | override suspend fun transformImageReader(imageReader: ImageReader, outputFormat: String): ImageReader = 22 | imageReader.changeSpeed(speed) 23 | 24 | override suspend fun transformAudioReader(audioReader: AudioReader, outputFormat: String): AudioReader = 25 | audioReader.changeSpeed(speed) 26 | } 27 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/task/StretchTask.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.task 2 | 3 | import io.github.shaksternano.borgar.core.media.ImageFrame 4 | import io.github.shaksternano.borgar.core.media.stretch 5 | import java.awt.image.BufferedImage 6 | 7 | class StretchTask( 8 | private val widthMultiplier: Double, 9 | private val heightMultiplier: Double, 10 | private val raw: Boolean, 11 | maxFileSize: Long, 12 | ) : SimpleMediaProcessingTask(maxFileSize) { 13 | 14 | override suspend fun transformImage(frame: ImageFrame): BufferedImage { 15 | val image = frame.content 16 | return image.stretch( 17 | (image.width * widthMultiplier).toInt(), 18 | (image.height * heightMultiplier).toInt(), 19 | raw, 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/task/SubwaySurfersTask.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.task 2 | 3 | import io.github.shaksternano.borgar.core.graphics.OverlayData 4 | import io.github.shaksternano.borgar.core.io.DataSource 5 | import io.github.shaksternano.borgar.core.media.* 6 | import io.github.shaksternano.borgar.core.media.reader.ImageReader 7 | import io.github.shaksternano.borgar.core.media.reader.firstContent 8 | import io.github.shaksternano.borgar.core.media.reader.readContent 9 | import kotlinx.coroutines.flow.Flow 10 | import java.awt.image.BufferedImage 11 | 12 | class SubwaySurfersTask( 13 | maxFileSize: Long, 14 | ) : MediaProcessingTask(maxFileSize) { 15 | 16 | override val config: MediaProcessingConfig = SimpleMediaProcessingConfig( 17 | processor = SubwaySurfersProcessor, 18 | ) 19 | } 20 | 21 | private object SubwaySurfersProcessor : ImageProcessor { 22 | 23 | override suspend fun constantData( 24 | firstFrame: ImageFrame, 25 | imageSource: Flow, 26 | outputFormat: String, 27 | ): SubwaySurfersData { 28 | val dataSource = DataSource.fromResource("media/overlay/subway_surfers_gameplay.mp4") 29 | val subwaySurfersReader = createImageReader(dataSource) 30 | val image = firstFrame.content 31 | val width = image.width 32 | val height = image.height 33 | val resized = subwaySurfersReader.firstContent().resizeHeight(height) 34 | val overlayData = getOverlayData(image, resized, width, 0, true) 35 | return SubwaySurfersData(subwaySurfersReader, overlayData) 36 | } 37 | 38 | override suspend fun transformImage(frame: ImageFrame, constantData: SubwaySurfersData): BufferedImage { 39 | val subwaySurfersReader = constantData.subwaySurfersReader 40 | val subwaySurfersFrame = subwaySurfersReader.readContent(frame.timestamp) 41 | val image = frame.content 42 | val resized = subwaySurfersFrame.resizeHeight(image.height) 43 | return overlay( 44 | image, 45 | resized, 46 | constantData.overlayData, 47 | false, 48 | ) 49 | } 50 | } 51 | 52 | private class SubwaySurfersData( 53 | val subwaySurfersReader: ImageReader, 54 | val overlayData: OverlayData, 55 | ) 56 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/task/TranscodeTask.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.task 2 | 3 | import io.github.shaksternano.borgar.core.media.MediaProcessingConfig 4 | 5 | class TranscodeTask( 6 | format: String, 7 | maxFileSize: Long, 8 | ) : MediaProcessingTask(maxFileSize) { 9 | 10 | override val config: MediaProcessingConfig = TranscodeConfig(format) 11 | } 12 | 13 | class TranscodeConfig( 14 | private val format: String, 15 | ) : MediaProcessingConfig { 16 | 17 | override fun transformOutputFormat(inputFormat: String): String = format 18 | } 19 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/task/UrlFileTask.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.task 2 | 3 | import io.github.shaksternano.borgar.core.io.DataSource 4 | 5 | class UrlFileTask( 6 | urls: Iterable, 7 | ) : BaseFileTask() { 8 | 9 | constructor(url: String) : this(listOf(url)) 10 | 11 | override val requireInput: Boolean = false 12 | private val urls = urls.map { 13 | DataSource.fromUrl( 14 | url = it, 15 | sendUrl = true, 16 | ) 17 | } 18 | 19 | override suspend fun run(input: List): List = urls 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/util/AnyUtil.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.util 2 | 3 | import java.text.DecimalFormat 4 | import kotlin.reflect.KClass 5 | 6 | val FLOAT_FORMAT = DecimalFormat("0.#") 7 | 8 | val T.kClass: KClass 9 | get() = this::class 10 | 11 | fun hash(vararg objects: Any?): Int { 12 | return if (objects.size == 1) { 13 | objects.single().hashCode() 14 | } else { 15 | objects.contentHashCode() 16 | } 17 | } 18 | 19 | fun T.asSingletonList(): List = listOf(this) 20 | 21 | val Any.formatted: String 22 | get() = when (this) { 23 | is Float -> FLOAT_FORMAT.format(this) 24 | is Double -> FLOAT_FORMAT.format(this) 25 | is Displayed -> displayName 26 | else -> toString() 27 | } 28 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/util/ChannelEnvironment.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.util 2 | 3 | enum class ChannelEnvironment( 4 | val displayName: String, 5 | val entityType: String, 6 | ) { 7 | GUILD("Guild", "guild"), 8 | 9 | /** 10 | * Direct message with this bot. 11 | */ 12 | DIRECT_MESSAGE("Direct Message", "user"), 13 | 14 | /** 15 | * Direct message between two other users, not with this bot. 16 | */ 17 | PRIVATE("Direct Message", "private"), 18 | GROUP("Group", "group"), 19 | ; 20 | 21 | companion object { 22 | val ALL: Set = entries.toSet() 23 | 24 | fun fromEntityType(entityType: String): ChannelEnvironment? = 25 | entries.find { it.entityType == entityType } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/util/Displayed.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.util 2 | 3 | interface Displayed { 4 | 5 | val displayName: String 6 | } 7 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/util/DurationUtil.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.util 2 | 3 | import kotlin.math.max 4 | import kotlin.time.Duration 5 | import kotlin.time.Duration.Companion.nanoseconds 6 | 7 | operator fun Duration.rem(scale: Long): Duration = 8 | (inWholeNanoseconds % scale).nanoseconds 9 | 10 | fun Duration.circular(total: Duration): Duration = 11 | this % max(total.inWholeNanoseconds, 1) 12 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/util/Environment.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.util 2 | 3 | import io.github.shaksternano.borgar.core.logger 4 | import java.nio.file.Path 5 | import kotlin.io.path.forEachLine 6 | 7 | private val customEnvVars: MutableMap = HashMap() 8 | 9 | fun loadEnv(path: Path) = path.forEachLine { 10 | if (it.isNotBlank()) { 11 | val envVar = it.split("=", limit = 2) 12 | if (envVar.size == 2) { 13 | val key = envVar[0].trim() 14 | val value = envVar[1].trim() 15 | if (key.isNotBlank() && value.isNotBlank()) { 16 | setEnvVar(key, value) 17 | } 18 | } else { 19 | logger.error("Invalid environment variable: $it") 20 | } 21 | } 22 | } 23 | 24 | fun getEnvVar(key: String): String? = 25 | customEnvVars[key] ?: System.getenv(key) 26 | 27 | fun setEnvVar(key: String, value: String) { 28 | customEnvVars[key] = value 29 | } 30 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/util/FunctionUtil.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.util 2 | 3 | fun ((T) -> T)?.then(after: (T) -> T): (T) -> T = 4 | { after(this?.invoke(it) ?: it) } 5 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/util/Identified.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.util 2 | 3 | interface Identified { 4 | 5 | val id: String 6 | } 7 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/util/JsonUtil.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.util 2 | 3 | import kotlinx.serialization.json.Json 4 | import kotlinx.serialization.json.JsonElement 5 | 6 | val JSON: Json = Json { 7 | explicitNulls = false 8 | ignoreUnknownKeys = true 9 | prettyPrint = true 10 | } 11 | 12 | fun prettyPrintJson(json: String): String { 13 | val jsonElement = Json.parseToJsonElement(json) 14 | return JSON.encodeToString(JsonElement.serializer(), jsonElement) 15 | } 16 | 17 | fun prettyPrintJsonCatching(json: String): String = runCatching { 18 | prettyPrintJson(json) 19 | }.getOrDefault(json) 20 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/util/MathUtil.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.util 2 | 3 | import kotlin.math.pow 4 | 5 | infix fun Int.pow(exponent: Int): Long = 6 | toDouble().pow(exponent).toLong() 7 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/shaksternano/borgar/core/util/Named.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.util 2 | 3 | interface Named { 4 | 5 | val name: String 6 | } 7 | -------------------------------------------------------------------------------- /core/src/main/resources/font/bitstream_vera_sans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaksternano/borgar/0cde2178a901167cbf7d7cd6a95b9d4480a9f4bb/core/src/main/resources/font/bitstream_vera_sans.ttf -------------------------------------------------------------------------------- /core/src/main/resources/font/futura_condensed_extra_bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaksternano/borgar/0cde2178a901167cbf7d7cd6a95b9d4480a9f4bb/core/src/main/resources/font/futura_condensed_extra_bold.otf -------------------------------------------------------------------------------- /core/src/main/resources/font/helvetica_neue.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaksternano/borgar/0cde2178a901167cbf7d7cd6a95b9d4480a9f4bb/core/src/main/resources/font/helvetica_neue.ttf -------------------------------------------------------------------------------- /core/src/main/resources/font/impact.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaksternano/borgar/0cde2178a901167cbf7d7cd6a95b9d4480a9f4bb/core/src/main/resources/font/impact.ttf -------------------------------------------------------------------------------- /core/src/main/resources/font/times.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaksternano/borgar/0cde2178a901167cbf7d7cd6a95b9d4480a9f4bb/core/src/main/resources/font/times.ttf -------------------------------------------------------------------------------- /core/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{HH:mm:ss.SSS} %boldCyan(%-34.-34thread) %red(%10.10X{jda.shard}) %boldGreen(%-15.-15logger{0}) 6 | %highlight(%-6level) %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /core/src/main/resources/media/background/live_reaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaksternano/borgar/0cde2178a901167cbf7d7cd6a95b9d4480a9f4bb/core/src/main/resources/media/background/live_reaction.png -------------------------------------------------------------------------------- /core/src/main/resources/media/containerimage/living_in_1984.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaksternano/borgar/0cde2178a901167cbf7d7cd6a95b9d4480a9f4bb/core/src/main/resources/media/containerimage/living_in_1984.png -------------------------------------------------------------------------------- /core/src/main/resources/media/containerimage/oh_my_goodness_gracious.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaksternano/borgar/0cde2178a901167cbf7d7cd6a95b9d4480a9f4bb/core/src/main/resources/media/containerimage/oh_my_goodness_gracious.gif -------------------------------------------------------------------------------- /core/src/main/resources/media/containerimage/sonic_says.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaksternano/borgar/0cde2178a901167cbf7d7cd6a95b9d4480a9f4bb/core/src/main/resources/media/containerimage/sonic_says.png -------------------------------------------------------------------------------- /core/src/main/resources/media/containerimage/soyjak_pointing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaksternano/borgar/0cde2178a901167cbf7d7cd6a95b9d4480a9f4bb/core/src/main/resources/media/containerimage/soyjak_pointing.png -------------------------------------------------------------------------------- /core/src/main/resources/media/containerimage/thinking_bubble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaksternano/borgar/0cde2178a901167cbf7d7cd6a95b9d4480a9f4bb/core/src/main/resources/media/containerimage/thinking_bubble.png -------------------------------------------------------------------------------- /core/src/main/resources/media/containerimage/thinking_bubble_edge_trimmed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaksternano/borgar/0cde2178a901167cbf7d7cd6a95b9d4480a9f4bb/core/src/main/resources/media/containerimage/thinking_bubble_edge_trimmed.png -------------------------------------------------------------------------------- /core/src/main/resources/media/containerimage/walmart_wanted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaksternano/borgar/0cde2178a901167cbf7d7cd6a95b9d4480a9f4bb/core/src/main/resources/media/containerimage/walmart_wanted.png -------------------------------------------------------------------------------- /core/src/main/resources/media/containerimage/who_did_this.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaksternano/borgar/0cde2178a901167cbf7d7cd6a95b9d4480a9f4bb/core/src/main/resources/media/containerimage/who_did_this.png -------------------------------------------------------------------------------- /core/src/main/resources/media/overlay/speech_bubble_1_full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaksternano/borgar/0cde2178a901167cbf7d7cd6a95b9d4480a9f4bb/core/src/main/resources/media/overlay/speech_bubble_1_full.png -------------------------------------------------------------------------------- /core/src/main/resources/media/overlay/speech_bubble_1_partial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaksternano/borgar/0cde2178a901167cbf7d7cd6a95b9d4480a9f4bb/core/src/main/resources/media/overlay/speech_bubble_1_partial.png -------------------------------------------------------------------------------- /core/src/main/resources/media/overlay/speech_bubble_2_full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaksternano/borgar/0cde2178a901167cbf7d7cd6a95b9d4480a9f4bb/core/src/main/resources/media/overlay/speech_bubble_2_full.png -------------------------------------------------------------------------------- /core/src/main/resources/media/overlay/speech_bubble_2_partial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaksternano/borgar/0cde2178a901167cbf7d7cd6a95b9d4480a9f4bb/core/src/main/resources/media/overlay/speech_bubble_2_partial.png -------------------------------------------------------------------------------- /core/src/main/resources/media/overlay/subway_surfers_gameplay.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaksternano/borgar/0cde2178a901167cbf7d7cd6a95b9d4480a9f4bb/core/src/main/resources/media/overlay/subway_surfers_gameplay.mp4 -------------------------------------------------------------------------------- /core/src/main/resources/shape/thinking_bubble_edge_trimmed.javaobject: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaksternano/borgar/0cde2178a901167cbf7d7cd6a95b9d4480a9f4bb/core/src/main/resources/shape/thinking_bubble_edge_trimmed.javaobject -------------------------------------------------------------------------------- /core/src/test/kotlin/io/github/shaksternano/borgar/core/io/IndexedInputStreamTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.io 2 | 3 | import org.junit.jupiter.api.Test 4 | import java.io.ByteArrayInputStream 5 | import kotlin.test.assertContentEquals 6 | import kotlin.test.assertEquals 7 | 8 | class IndexedInputStreamTest { 9 | 10 | @Test 11 | fun testReadUpdatesIndex() { 12 | val inputData = "Hello, World!".encodeToByteArray() 13 | val inputStream = ByteArrayInputStream(inputData).indexed() 14 | 15 | assertEquals(0, inputStream.nextIndex) 16 | 17 | // Read first byte 18 | val byte = inputStream.read() 19 | assertEquals('H'.code, byte) 20 | assertEquals(1, inputStream.nextIndex) 21 | 22 | // Read the next 5 bytes 23 | val buffer = ByteArray(5) 24 | inputStream.read(buffer) 25 | assertContentEquals("ello,".encodeToByteArray(), buffer) 26 | assertEquals(6, inputStream.nextIndex) 27 | } 28 | 29 | @Test 30 | fun testSkipUpdatesIndex() { 31 | val inputData = "Hello, World!".encodeToByteArray() 32 | val inputStream = ByteArrayInputStream(inputData).indexed() 33 | 34 | assertEquals(0, inputStream.nextIndex) 35 | 36 | // Skip 7 bytes 37 | val skipped = inputStream.skip(7) 38 | assertEquals(7, skipped) 39 | assertEquals(7, inputStream.nextIndex) 40 | 41 | // Read next byte 42 | val byte = inputStream.read() 43 | assertEquals('W'.code, byte) 44 | assertEquals(8, inputStream.nextIndex) 45 | } 46 | 47 | @Test 48 | fun testMarkAndReset() { 49 | val inputData = "Hello, World!".encodeToByteArray() 50 | val inputStream = ByteArrayInputStream(inputData).indexed() 51 | 52 | // Read 6 bytes 53 | inputStream.skip(6) 54 | assertEquals(6, inputStream.nextIndex) 55 | 56 | // Mark the current position 57 | inputStream.mark(100) 58 | 59 | // Read 5 more bytes 60 | inputStream.skip(5) 61 | assertEquals(11, inputStream.nextIndex) 62 | 63 | // Reset to a marked position 64 | inputStream.reset() 65 | assertEquals(6, inputStream.nextIndex) 66 | 67 | // Verify we're at the right position 68 | val byte = inputStream.read() 69 | assertEquals(' '.code, byte) // The character at position 6 is a space 70 | assertEquals(7, inputStream.nextIndex) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /core/src/test/kotlin/io/github/shaksternano/borgar/core/task/FileTaskTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.core.task 2 | 3 | import io.github.shaksternano.borgar.core.io.DataSource 4 | import kotlinx.coroutines.test.runTest 5 | import org.junit.jupiter.api.assertThrows 6 | import kotlin.test.Test 7 | import kotlin.test.assertEquals 8 | 9 | class FileTaskTest { 10 | 11 | @Test 12 | fun canChainTasks() = runTest { 13 | val task1 = StringConcatTask("b") 14 | val task2 = StringConcatTask("c") 15 | val inputBytes = "a".encodeToByteArray() 16 | val input = DataSource.fromBytes("input", inputBytes) 17 | val chained = task1 then task2 18 | val result = chained.run(listOf(input)) 19 | assertEquals(1, result.size) 20 | val output = result.first() 21 | val outputBytes = output.newStream().readAllBytes() 22 | val outputString = String(outputBytes) 23 | assertEquals("abc", outputString) 24 | } 25 | 26 | @Test 27 | fun taskMustRequireInput() = runTest { 28 | val task1 = StringConcatTask("b") 29 | val task2 = NoInputTask() 30 | assertThrows { 31 | task1 then task2 32 | } 33 | } 34 | } 35 | 36 | private class StringConcatTask( 37 | private val toConcat: String, 38 | ) : FileTask { 39 | override val requireInput: Boolean = true 40 | 41 | override suspend fun run(input: List): List { 42 | return input.map { 43 | val string = String(it.newStream().readAllBytes()) 44 | val output = string + toConcat 45 | DataSource.fromBytes("string", output.encodeToByteArray()) 46 | } 47 | } 48 | } 49 | 50 | private class NoInputTask : FileTask { 51 | 52 | override val requireInput: Boolean = false 53 | 54 | override suspend fun run(input: List): List = throw IllegalStateException() 55 | } 56 | -------------------------------------------------------------------------------- /discord/build.gradle.kts: -------------------------------------------------------------------------------- 1 | val jdaVersion: String by project 2 | val jdaKtxVersion: String by project 3 | 4 | dependencies { 5 | api(project(":messaging")) 6 | 7 | implementation("net.dv8tion:JDA:$jdaVersion") { 8 | exclude(module = "opus-java") 9 | } 10 | implementation("club.minnced:jda-ktx:$jdaKtxVersion") 11 | 12 | testImplementation(kotlin("test")) 13 | } 14 | -------------------------------------------------------------------------------- /discord/src/main/kotlin/io/github/shaksternano/borgar/discord/entity/DiscordCustomEmoji.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.discord.entity 2 | 3 | import io.github.shaksternano.borgar.messaging.BotManager 4 | import io.github.shaksternano.borgar.messaging.entity.BaseEntity 5 | import io.github.shaksternano.borgar.messaging.entity.CustomEmoji 6 | 7 | class DiscordCustomEmoji( 8 | discordEmoji: net.dv8tion.jda.api.entities.emoji.CustomEmoji, 9 | override val manager: BotManager, 10 | ) : CustomEmoji, BaseEntity() { 11 | 12 | override val id: String = discordEmoji.id 13 | override val name: String = discordEmoji.name 14 | override val imageUrl: String = discordEmoji.imageUrl 15 | override val asMention: String = discordEmoji.asMention 16 | override val asBasicMention: String = ":${name}:" 17 | 18 | override fun toString(): String { 19 | return "DiscordCustomEmoji(" + 20 | "name='$name'" + 21 | ", id='$id'" + 22 | ", imageUrl='$imageUrl'" + 23 | ")" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /discord/src/main/kotlin/io/github/shaksternano/borgar/discord/entity/DiscordGroup.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.discord.entity 2 | 3 | import dev.minn.jda.ktx.coroutines.await 4 | import io.github.shaksternano.borgar.discord.DiscordManager 5 | import io.github.shaksternano.borgar.messaging.BotManager 6 | import io.github.shaksternano.borgar.messaging.entity.BaseEntity 7 | import io.github.shaksternano.borgar.messaging.entity.Group 8 | import net.dv8tion.jda.api.entities.channel.concrete.GroupChannel 9 | 10 | data class DiscordGroup( 11 | private val discordGroupChannel: GroupChannel, 12 | ) : Group, BaseEntity() { 13 | 14 | override val id: String = discordGroupChannel.id 15 | override val manager: BotManager = DiscordManager[discordGroupChannel.jda] 16 | override val name: String? = discordGroupChannel.name.ifBlank { null } 17 | override val ownerId: String = discordGroupChannel.ownerId 18 | override val iconUrl: String? = discordGroupChannel.iconUrl 19 | 20 | override suspend fun isMember(userId: String): Boolean = 21 | userId == discordGroupChannel.retrieveOwner().await().id 22 | } 23 | -------------------------------------------------------------------------------- /discord/src/main/kotlin/io/github/shaksternano/borgar/discord/entity/DiscordMember.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.discord.entity 2 | 3 | import io.github.shaksternano.borgar.discord.DiscordManager 4 | import io.github.shaksternano.borgar.discord.ifNotDetachedOrElse 5 | import io.github.shaksternano.borgar.messaging.BotManager 6 | import io.github.shaksternano.borgar.messaging.entity.Guild 7 | import io.github.shaksternano.borgar.messaging.entity.Member 8 | import io.github.shaksternano.borgar.messaging.entity.Role 9 | import io.github.shaksternano.borgar.messaging.entity.User 10 | import kotlinx.coroutines.flow.Flow 11 | import kotlinx.coroutines.flow.asFlow 12 | import kotlinx.coroutines.flow.emptyFlow 13 | import java.time.OffsetDateTime 14 | 15 | data class DiscordMember( 16 | private val discordMember: net.dv8tion.jda.api.entities.Member, 17 | ) : Member, DiscordPermissionHolder(discordMember) { 18 | 19 | override val id: String = discordMember.id 20 | override val manager: BotManager = DiscordManager[discordMember.jda] 21 | override val user: User = DiscordUser(discordMember.user) 22 | override val roles: Flow = discordMember.ifNotDetachedOrElse(emptyFlow()) { 23 | discordMember.roles.map { DiscordRole(it) }.asFlow() 24 | } 25 | override val timeoutEnd: OffsetDateTime? = discordMember.timeOutEnd 26 | private val guild: Guild = DiscordGuild(discordMember.guild) 27 | override val effectiveName: String = discordMember.effectiveName 28 | override val effectiveAvatarUrl: String = "${discordMember.effectiveAvatarUrl}?size=1024" 29 | override val asMention: String = discordMember.asMention 30 | override val asBasicMention: String = "@${discordMember.effectiveName}" 31 | 32 | override suspend fun getGuild(): Guild = guild 33 | } 34 | -------------------------------------------------------------------------------- /discord/src/main/kotlin/io/github/shaksternano/borgar/discord/entity/DiscordMentionable.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.discord.entity 2 | 3 | import io.github.shaksternano.borgar.discord.DiscordManager 4 | import io.github.shaksternano.borgar.discord.entity.channel.DiscordChannel 5 | import io.github.shaksternano.borgar.messaging.BotManager 6 | import io.github.shaksternano.borgar.messaging.entity.Mentionable 7 | import net.dv8tion.jda.api.JDA 8 | import net.dv8tion.jda.api.entities.Member 9 | import net.dv8tion.jda.api.entities.Role 10 | import net.dv8tion.jda.api.entities.User 11 | import net.dv8tion.jda.api.entities.channel.Channel 12 | import net.dv8tion.jda.api.entities.emoji.CustomEmoji 13 | import net.dv8tion.jda.api.interactions.InteractionContextType 14 | 15 | class DiscordMentionable( 16 | mentionable: net.dv8tion.jda.api.entities.IMentionable, 17 | jda: JDA, 18 | ) : Mentionable { 19 | 20 | companion object { 21 | fun create( 22 | mentionable: net.dv8tion.jda.api.entities.IMentionable, 23 | jda: JDA, 24 | context: InteractionContextType = InteractionContextType.UNKNOWN, 25 | ): Mentionable = when (mentionable) { 26 | is User -> DiscordUser(mentionable) 27 | is Member -> DiscordMember(mentionable) 28 | is Channel -> DiscordChannel.create(mentionable, context) 29 | is Role -> DiscordRole(mentionable) 30 | is CustomEmoji -> DiscordCustomEmoji(mentionable, DiscordManager[jda]) 31 | else -> DiscordMentionable(mentionable, jda) 32 | } 33 | } 34 | 35 | override val id: String = mentionable.id 36 | override val manager: BotManager = DiscordManager[jda] 37 | override val asMention: String = mentionable.asMention 38 | override val asBasicMention: String = asMention 39 | } 40 | -------------------------------------------------------------------------------- /discord/src/main/kotlin/io/github/shaksternano/borgar/discord/entity/DiscordPermissionHolder.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.discord.entity 2 | 3 | import io.github.shaksternano.borgar.discord.entity.channel.DiscordChannel 4 | import io.github.shaksternano.borgar.discord.util.toDiscord 5 | import io.github.shaksternano.borgar.messaging.command.Permission 6 | import io.github.shaksternano.borgar.messaging.entity.BaseEntity 7 | import io.github.shaksternano.borgar.messaging.entity.PermissionHolder 8 | import io.github.shaksternano.borgar.messaging.entity.channel.Channel 9 | import net.dv8tion.jda.api.entities.IPermissionHolder 10 | import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel 11 | import net.dv8tion.jda.api.entities.detached.IDetachableEntity 12 | 13 | abstract class DiscordPermissionHolder( 14 | private val permissionHolder: IPermissionHolder, 15 | ) : PermissionHolder, BaseEntity() { 16 | 17 | private val isDetached: Boolean = permissionHolder is IDetachableEntity && permissionHolder.isDetached 18 | 19 | override suspend fun hasPermission(permissions: Set): Boolean = 20 | if (isDetached) { 21 | true 22 | } else { 23 | permissionHolder.hasPermission(permissions.map { it.toDiscord() }) 24 | } 25 | 26 | override suspend fun hasPermission(permissions: Set, channel: Channel): Boolean = 27 | if (isDetached) { 28 | true 29 | } else if (channel is DiscordChannel && channel.discordChannel is GuildChannel) { 30 | permissionHolder.hasPermission(channel.discordChannel, permissions.map { it.toDiscord() }) 31 | } else { 32 | hasPermission(permissions) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /discord/src/main/kotlin/io/github/shaksternano/borgar/discord/entity/DiscordRole.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.discord.entity 2 | 3 | import io.github.shaksternano.borgar.discord.DiscordManager 4 | import io.github.shaksternano.borgar.messaging.BotManager 5 | import io.github.shaksternano.borgar.messaging.entity.Role 6 | 7 | data class DiscordRole( 8 | private val discordRole: net.dv8tion.jda.api.entities.Role, 9 | ) : Role, DiscordPermissionHolder(discordRole) { 10 | 11 | override val id: String = discordRole.id 12 | override val manager: BotManager = DiscordManager[discordRole.jda] 13 | override val name: String = discordRole.name 14 | override val asMention: String = discordRole.asMention 15 | override val asBasicMention: String = 16 | if (discordRole.isPublicRole) discordRole.name 17 | else "@${discordRole.name}" 18 | } 19 | -------------------------------------------------------------------------------- /discord/src/main/kotlin/io/github/shaksternano/borgar/discord/entity/DiscordSticker.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.discord.entity 2 | 3 | import io.github.shaksternano.borgar.discord.DiscordManager 4 | import io.github.shaksternano.borgar.messaging.BotManager 5 | import io.github.shaksternano.borgar.messaging.entity.Sticker 6 | import net.dv8tion.jda.api.JDA 7 | 8 | data class DiscordSticker( 9 | private val discordSticker: net.dv8tion.jda.api.entities.sticker.Sticker, 10 | private val jda: JDA, 11 | ) : Sticker { 12 | 13 | override val id: String = discordSticker.id 14 | override val manager: BotManager = DiscordManager[jda] 15 | override val name: String = discordSticker.name 16 | override val imageUrl: String = discordSticker.iconUrl 17 | } 18 | -------------------------------------------------------------------------------- /discord/src/main/kotlin/io/github/shaksternano/borgar/discord/entity/DiscordUser.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.discord.entity 2 | 3 | import dev.minn.jda.ktx.coroutines.await 4 | import io.github.shaksternano.borgar.discord.DiscordManager 5 | import io.github.shaksternano.borgar.messaging.BotManager 6 | import io.github.shaksternano.borgar.messaging.entity.BaseEntity 7 | import io.github.shaksternano.borgar.messaging.entity.User 8 | 9 | data class DiscordUser( 10 | private val discordUser: net.dv8tion.jda.api.entities.User, 11 | ) : User, BaseEntity() { 12 | 13 | override val id: String = discordUser.id 14 | override val manager: BotManager = DiscordManager[discordUser.jda] 15 | override val name: String = discordUser.name 16 | override val effectiveName: String = discordUser.effectiveName 17 | override val effectiveAvatarUrl: String = "${discordUser.effectiveAvatarUrl}?size=1024" 18 | override val isSelf: Boolean = discordUser.jda.selfUser == discordUser 19 | override val isBot: Boolean = discordUser.isBot 20 | override val asMention: String = discordUser.asMention 21 | override val asBasicMention: String = "@${discordUser.effectiveName}" 22 | 23 | override suspend fun getBannerUrl(): String? = 24 | discordUser.getBannerUrl() 25 | } 26 | 27 | suspend fun net.dv8tion.jda.api.entities.User.getBannerUrl(): String? = 28 | retrieveProfile() 29 | .useCache(false) 30 | .await() 31 | .bannerUrl 32 | ?.let { 33 | "$it?size=1024" 34 | } 35 | -------------------------------------------------------------------------------- /discord/src/main/kotlin/io/github/shaksternano/borgar/discord/interaction/DiscordInteractionCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.discord.interaction 2 | 3 | import io.github.shaksternano.borgar.core.util.Named 4 | import net.dv8tion.jda.api.interactions.InteractionContextType 5 | import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback 6 | 7 | interface DiscordInteractionCommand : Named { 8 | 9 | val environment: Set 10 | get() = InteractionContextType.ALL 11 | 12 | suspend fun respond(event: T): Any? 13 | 14 | suspend fun onResponseSend( 15 | responseData: Any?, 16 | event: T, 17 | ) = Unit 18 | } 19 | -------------------------------------------------------------------------------- /discord/src/main/kotlin/io/github/shaksternano/borgar/discord/interaction/DiscordInteractionCommandHandler.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.discord.interaction 2 | 3 | import dev.minn.jda.ktx.coroutines.await 4 | import io.github.shaksternano.borgar.discord.DiscordManager 5 | import io.github.shaksternano.borgar.messaging.command.handleError 6 | import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback 7 | 8 | suspend fun handleInteractionCommand(command: DiscordInteractionCommand, event: T) = 9 | runCatching { 10 | val responseData = command.respond(event) 11 | command.onResponseSend(responseData, event) 12 | }.onFailure { 13 | val responseContent = handleError(it, DiscordManager[event.jda]) 14 | event.reply(responseContent).await() 15 | } 16 | -------------------------------------------------------------------------------- /discord/src/main/kotlin/io/github/shaksternano/borgar/discord/interaction/message/CommandModalInteractionCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.discord.interaction.message 2 | 3 | import dev.minn.jda.ktx.coroutines.await 4 | import dev.minn.jda.ktx.interactions.components.Modal 5 | import dev.minn.jda.ktx.interactions.components.TextInput 6 | import io.github.shaksternano.borgar.discord.entity.DiscordMessage 7 | import io.github.shaksternano.borgar.discord.interaction.modal.RunCommandInteractionCommand 8 | import io.github.shaksternano.borgar.messaging.MessagingPlatform 9 | import io.github.shaksternano.borgar.messaging.util.setSelectedMessage 10 | import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent 11 | import net.dv8tion.jda.api.interactions.components.ActionRow 12 | import net.dv8tion.jda.api.interactions.components.text.TextInputStyle 13 | 14 | object CommandModalInteractionCommand : DiscordMessageInteractionCommand { 15 | 16 | override val name: String = "Run command" 17 | 18 | override suspend fun respond(event: MessageContextInteractionEvent): Any? { 19 | val command = TextInput( 20 | id = RunCommandInteractionCommand.TEXT_INPUT_ID, 21 | label = "Command", 22 | style = TextInputStyle.SHORT, 23 | ) { 24 | placeholder = "Enter the command you want to execute on this message" 25 | builder.minLength = 1 26 | } 27 | val modal = Modal( 28 | id = RunCommandInteractionCommand.name, 29 | title = name, 30 | ) { 31 | components += ActionRow.of(command) 32 | } 33 | val channelId = event.channelId 34 | if (channelId != null) { 35 | setSelectedMessage( 36 | userId = event.user.id, 37 | channelId = channelId, 38 | platform = MessagingPlatform.DISCORD, 39 | message = DiscordMessage(event.target), 40 | ) 41 | } 42 | event.replyModal(modal).await() 43 | return null 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /discord/src/main/kotlin/io/github/shaksternano/borgar/discord/interaction/message/DiscordMessageInteractionCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.discord.interaction.message 2 | 3 | import io.github.shaksternano.borgar.discord.interaction.DiscordInteractionCommand 4 | import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent 5 | 6 | interface DiscordMessageInteractionCommand : DiscordInteractionCommand 7 | -------------------------------------------------------------------------------- /discord/src/main/kotlin/io/github/shaksternano/borgar/discord/interaction/message/DiscordMessageInteractionCommands.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.discord.interaction.message 2 | 3 | import dev.minn.jda.ktx.coroutines.await 4 | import io.github.shaksternano.borgar.core.logger 5 | import io.github.shaksternano.borgar.discord.interaction.handleInteractionCommand 6 | import io.github.shaksternano.borgar.messaging.command.registerCommands 7 | import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent 8 | 9 | val MESSAGE_INTERACTION_COMMANDS: Map = registerCommands( 10 | "Discord message interaction command", 11 | GifInteractionCommand, 12 | DownloadInteractionCommand, 13 | CommandModalInteractionCommand, 14 | SelectMessageInteractionCommand, 15 | ) 16 | 17 | suspend fun handleMessageInteraction(event: MessageContextInteractionEvent) { 18 | val commandName = event.name.lowercase() 19 | val command = MESSAGE_INTERACTION_COMMANDS[commandName] 20 | if (command == null) { 21 | logger.error("Unknown message interaction command: $commandName") 22 | event.reply("Unknown command!") 23 | .setEphemeral(true) 24 | .await() 25 | return 26 | } 27 | handleInteractionCommand(command, event) 28 | } 29 | -------------------------------------------------------------------------------- /discord/src/main/kotlin/io/github/shaksternano/borgar/discord/interaction/message/SelectMessageInteractionCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.discord.interaction.message 2 | 3 | import dev.minn.jda.ktx.coroutines.await 4 | import io.github.shaksternano.borgar.discord.entity.DiscordMessage 5 | import io.github.shaksternano.borgar.messaging.MessagingPlatform 6 | import io.github.shaksternano.borgar.messaging.util.setSelectedMessage 7 | import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent 8 | 9 | object SelectMessageInteractionCommand : DiscordMessageInteractionCommand { 10 | 11 | override val name: String = "Select message" 12 | 13 | override suspend fun respond(event: MessageContextInteractionEvent): Any? { 14 | val message = event.target 15 | setSelectedMessage( 16 | userId = event.user.id, 17 | channelId = event.target.channelId, 18 | platform = MessagingPlatform.DISCORD, 19 | message = DiscordMessage(event.target), 20 | ) 21 | event.reply("The message ${message.jumpUrl} has been selected for your next command.") 22 | .setEphemeral(true) 23 | .await() 24 | return null 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /discord/src/main/kotlin/io/github/shaksternano/borgar/discord/interaction/modal/DiscordModalInteractionCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.discord.interaction.modal 2 | 3 | import io.github.shaksternano.borgar.discord.interaction.DiscordInteractionCommand 4 | import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent 5 | 6 | typealias DiscordModalInteractionCommand = DiscordInteractionCommand 7 | -------------------------------------------------------------------------------- /discord/src/main/kotlin/io/github/shaksternano/borgar/discord/interaction/modal/DiscordModalInteractionCommands.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.discord.interaction.modal 2 | 3 | import dev.minn.jda.ktx.coroutines.await 4 | import io.github.shaksternano.borgar.core.logger 5 | import io.github.shaksternano.borgar.discord.interaction.handleInteractionCommand 6 | import io.github.shaksternano.borgar.messaging.command.registerCommands 7 | import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent 8 | 9 | val MODAL_INTERACTION_COMMANDS: Map = registerCommands( 10 | "Discord modal interaction command", 11 | RunCommandInteractionCommand, 12 | ) 13 | 14 | suspend fun handleModalInteraction(event: ModalInteractionEvent) { 15 | val commandName = event.modalId.lowercase() 16 | val command = MODAL_INTERACTION_COMMANDS[commandName] 17 | if (command == null) { 18 | logger.error("Unknown message interaction command: $commandName") 19 | event.reply("Unknown command!") 20 | .setEphemeral(true) 21 | .await() 22 | return 23 | } 24 | handleInteractionCommand(command, event) 25 | } 26 | -------------------------------------------------------------------------------- /discord/src/main/kotlin/io/github/shaksternano/borgar/discord/interaction/user/DiscordUserInteractionCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.discord.interaction.user 2 | 3 | import io.github.shaksternano.borgar.discord.interaction.DiscordInteractionCommand 4 | import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEvent 5 | 6 | interface DiscordUserInteractionCommand : DiscordInteractionCommand 7 | -------------------------------------------------------------------------------- /discord/src/main/kotlin/io/github/shaksternano/borgar/discord/interaction/user/DiscordUserInteractionCommands.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.discord.interaction.user 2 | 3 | import dev.minn.jda.ktx.coroutines.await 4 | import io.github.shaksternano.borgar.core.logger 5 | import io.github.shaksternano.borgar.discord.interaction.handleInteractionCommand 6 | import io.github.shaksternano.borgar.messaging.command.registerCommands 7 | import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEvent 8 | 9 | val USER_INTERACTION_COMMANDS: Map = registerCommands( 10 | "Discord user interaction command", 11 | UserAvatarInteractionCommand, 12 | MemberAvatarInteractionCommand, 13 | UserBannerInteractionCommand, 14 | ) 15 | 16 | suspend fun handleUserInteraction(event: UserContextInteractionEvent) { 17 | val commandName = event.name.lowercase() 18 | val command = USER_INTERACTION_COMMANDS[commandName] 19 | if (command == null) { 20 | logger.error("Unknown user interaction command: $commandName") 21 | event.reply("Unknown command!") 22 | .setEphemeral(true) 23 | .await() 24 | return 25 | } 26 | handleInteractionCommand(command, event) 27 | } 28 | -------------------------------------------------------------------------------- /discord/src/main/kotlin/io/github/shaksternano/borgar/discord/interaction/user/MemberAvatarInteractionCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.discord.interaction.user 2 | 3 | import dev.minn.jda.ktx.coroutines.await 4 | import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEvent 5 | import net.dv8tion.jda.api.interactions.InteractionContextType 6 | 7 | object MemberAvatarInteractionCommand : DiscordUserInteractionCommand { 8 | 9 | override val name: String = "Get user server avatar" 10 | override val environment: Set = setOf(InteractionContextType.GUILD) 11 | 12 | override suspend fun respond(event: UserContextInteractionEvent): Any? { 13 | val user = event.target 14 | val avatarUrl = event.guild 15 | ?.retrieveMember(user) 16 | ?.useCache(false) 17 | ?.await() 18 | ?.effectiveAvatarUrl 19 | ?: user.effectiveAvatarUrl 20 | event.reply("$avatarUrl?size=1024") 21 | .setEphemeral(true) 22 | .await() 23 | return null 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /discord/src/main/kotlin/io/github/shaksternano/borgar/discord/interaction/user/UserAvatarInteractionCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.discord.interaction.user 2 | 3 | import dev.minn.jda.ktx.coroutines.await 4 | import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEvent 5 | 6 | object UserAvatarInteractionCommand : DiscordUserInteractionCommand { 7 | 8 | override val name: String = "Get user avatar" 9 | 10 | override suspend fun respond(event: UserContextInteractionEvent): Any? { 11 | val avatarUrl = event.target.effectiveAvatarUrl 12 | event.reply("$avatarUrl?size=1024") 13 | .setEphemeral(true) 14 | .await() 15 | return null 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /discord/src/main/kotlin/io/github/shaksternano/borgar/discord/interaction/user/UserBannerInteractionCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.discord.interaction.user 2 | 3 | import dev.minn.jda.ktx.coroutines.await 4 | import io.github.shaksternano.borgar.discord.entity.getBannerUrl 5 | import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEvent 6 | 7 | object UserBannerInteractionCommand : DiscordUserInteractionCommand { 8 | 9 | override val name: String = "Get user banner" 10 | 11 | override suspend fun respond(event: UserContextInteractionEvent): Any? { 12 | val user = event.target 13 | event.reply(event.target.getBannerUrl() ?: "${user.asMention} has no banner.") 14 | .setEphemeral(true) 15 | .await() 16 | return null 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /discord/src/main/kotlin/io/github/shaksternano/borgar/discord/util/DiscordPermissions.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.discord.util 2 | 3 | import io.github.shaksternano.borgar.messaging.command.Permission 4 | 5 | fun Permission.toDiscord(): net.dv8tion.jda.api.Permission = when (this) { 6 | Permission.MANAGE_GUILD_EXPRESSIONS -> net.dv8tion.jda.api.Permission.MANAGE_GUILD_EXPRESSIONS 7 | } 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | db: 5 | image: postgres 6 | environment: 7 | POSTGRES_USER: ${POSTGRESQL_USERNAME} 8 | POSTGRES_PASSWORD: ${POSTGRESQL_PASSWORD} 9 | PGDATA: /data/postgres 10 | volumes: 11 | - db:/data/postgres 12 | ports: 13 | - ${POSTGRESQL_PORTS} 14 | networks: 15 | - db 16 | restart: unless-stopped 17 | 18 | cobalt-api: 19 | image: ghcr.io/imputnet/cobalt:10 20 | 21 | init: true 22 | read_only: true 23 | restart: unless-stopped 24 | 25 | ports: 26 | - "9000:9000/tcp" 27 | # if you use a reverse proxy (such as nginx), 28 | # uncomment the next line and remove the one above (9000:9000/tcp): 29 | # - 127.0.0.1:9000:9000 30 | 31 | environment: 32 | # replace https://api.url.example with your instance's url 33 | # or else tunneling functionality won't work properly 34 | API_URL: "http://localhost:9000" 35 | 36 | # if you want to use cookies for fetching data from services, 37 | # uncomment the next line & volumes section 38 | # COOKIE_PATH: "/cookies.json" 39 | 40 | # it's recommended to configure bot protection or api keys if the instance is public, 41 | # see /docs/protect-an-instance.md for more info 42 | 43 | # see /docs/run-an-instance.md for more variables that you can use here 44 | 45 | labels: 46 | - com.centurylinklabs.watchtower.scope=cobalt 47 | 48 | # uncomment only if you use the COOKIE_PATH variable 49 | # volumes: 50 | # - ./cookies.json:/cookies.json 51 | 52 | # watchtower updates the cobalt image automatically 53 | watchtower: 54 | image: ghcr.io/containrrr/watchtower 55 | restart: unless-stopped 56 | command: --cleanup --scope cobalt --interval 900 --include-restarting 57 | volumes: 58 | - /var/run/docker.sock:/var/run/docker.sock 59 | 60 | networks: 61 | db: 62 | driver: bridge 63 | 64 | volumes: 65 | db: 66 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Done to increase the memory available to Gradle 2 | org.gradle.jvmargs = -Xmx2G 3 | 4 | # Dependencies 5 | kotlinVersion = 2.1.21 6 | kotlinxCoroutinesVersion = 1.10.2 7 | kotlinxIoVersion = 0.7.0 8 | kotlinxAtomicFuVersion = 0.27.0 9 | ktorVersion = 3.1.3 10 | jdaVersion = 5.5.1 11 | jdaKtxVersion = 0.12.0 12 | logbackVersion = 1.5.18 13 | guavaVersion = 33.4.8 14 | commonsIoVersion = 2.19.0 15 | ulidKotlinVersion = 1.3.0 16 | javacvVersion = 1.5.11 17 | scrimageVersion = 4.3.1 18 | gifKtVersion = 0.1.0-SNAPSHOT 19 | twelveMonkeysVersion = 3.12.0 20 | image4jVersion = 0.7.2 21 | pdfBoxVersion = 3.0.5 22 | reflectionsVersion = 0.10.2 23 | exposedVersion = 0.61.0 24 | postgreSqlVersion = 42.7.5 25 | junitVersion = 5.12.2 26 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaksternano/borgar/0cde2178a901167cbf7d7cd6a95b9d4480a9f4bb/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.14-bin.zip 4 | zipStoreBase = GRADLE_USER_HOME 5 | zipStorePath = wrapper/dists 6 | -------------------------------------------------------------------------------- /images/image_caption_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaksternano/borgar/0cde2178a901167cbf7d7cd6a95b9d4480a9f4bb/images/image_caption_example.png -------------------------------------------------------------------------------- /messaging/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api(project(":core")) 3 | 4 | testImplementation(kotlin("test")) 5 | } 6 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/BotManager.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging 2 | 3 | import io.github.shaksternano.borgar.messaging.command.Permission 4 | import io.github.shaksternano.borgar.messaging.entity.* 5 | import io.github.shaksternano.borgar.messaging.entity.channel.Channel 6 | import kotlinx.coroutines.flow.Flow 7 | import kotlin.time.Duration 8 | 9 | const val BOT_STATUS: String = "fortnite battle pass" 10 | 11 | private val botManagersMutable: MutableList = mutableListOf() 12 | val BOT_MANAGERS: List = botManagersMutable 13 | 14 | interface BotManager { 15 | 16 | val platform: MessagingPlatform 17 | val selfId: String 18 | val ownerId: String 19 | val maxMessageContentLength: Int 20 | val maxFileSize: Long 21 | val maxFilesPerMessage: Int 22 | val emojiTypedRegex: Regex 23 | val typingDuration: Duration 24 | val commandAutoCompleteMaxSuggestions: Int 25 | 26 | suspend fun getSelf(): User 27 | 28 | suspend fun getUser(id: String): User? 29 | 30 | suspend fun getChannel(id: String): Channel? 31 | 32 | suspend fun getGuild(id: String): Guild? 33 | 34 | suspend fun getGroup(id: String): Group? 35 | 36 | suspend fun getGuildCount(): Int 37 | 38 | fun getCustomEmojis(content: String): Flow 39 | 40 | fun getMentionedUsers(content: String): Flow 41 | 42 | fun getMentionedChannels(content: String): Flow 43 | 44 | fun getMentionedRoles(content: String): Flow 45 | 46 | fun getEmojiName(typedEmoji: String): String 47 | 48 | fun emojiAsTyped(emoji: String): String 49 | 50 | fun getPermissionName(permission: Permission): String 51 | } 52 | 53 | fun registerBotManager(botManager: BotManager) { 54 | botManagersMutable += botManager 55 | } 56 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/Messaging.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging 2 | 3 | import io.github.shaksternano.borgar.messaging.command.CommandConfig 4 | import io.github.shaksternano.borgar.messaging.command.DerpibooruCommand 5 | import io.github.shaksternano.borgar.messaging.command.executeCommands 6 | import io.github.shaksternano.borgar.messaging.command.sendResponses 7 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 8 | import kotlinx.coroutines.coroutineScope 9 | import kotlinx.coroutines.launch 10 | 11 | suspend fun initMessaging() { 12 | DerpibooruCommand.loadTags() 13 | } 14 | 15 | suspend fun CommandEvent.executeAndRespond(commandConfigs: List) { 16 | val channel = getChannel() 17 | val environment = channel.environment 18 | ephemeralReply = commandConfigs.any { it.command.ephemeralReply } 19 | val (responses, executable) = coroutineScope { 20 | val anyDefer = commandConfigs.any { it.command.deferReply } 21 | if (anyDefer) launch { 22 | deferReply() 23 | } 24 | executeCommands(commandConfigs, environment, this@executeAndRespond) 25 | } 26 | sendResponses(responses, executable, this, channel) 27 | } 28 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/MessagingPlatform.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging 2 | 3 | import io.github.shaksternano.borgar.core.util.Displayed 4 | import io.github.shaksternano.borgar.core.util.Identified 5 | 6 | enum class MessagingPlatform( 7 | override val id: String, 8 | override val displayName: String 9 | ) : Identified, Displayed { 10 | DISCORD("discord", "Discord"), 11 | REVOLT("revolt", "Revolt"), 12 | } 13 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/builder/MessageBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.builder 2 | 3 | import io.github.shaksternano.borgar.core.io.DataSource 4 | import io.github.shaksternano.borgar.messaging.command.CommandResponse 5 | 6 | data class MessageCreateBuilder( 7 | var content: String = "", 8 | val files: MutableList = mutableListOf(), 9 | val referencedMessageIds: MutableList = mutableListOf(), 10 | var suppressEmbeds: Boolean = false, 11 | var username: String? = null, 12 | var avatarUrl: String? = null, 13 | ) { 14 | 15 | fun fromCommandResponse(response: CommandResponse) { 16 | content = response.content 17 | files.addAll(response.files) 18 | suppressEmbeds = response.suppressEmbeds 19 | } 20 | } 21 | 22 | data class MessageEditBuilder( 23 | var content: String? = null, 24 | val files: MutableList? = mutableListOf(), 25 | ) 26 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/ApiFilesCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.task.FileTask 4 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 5 | 6 | abstract class ApiFilesCommand( 7 | private val fileCount: Int, 8 | ) : FileCommand( 9 | CommandArgumentInfo( 10 | key = "tags", 11 | description = "The tags to search for.", 12 | type = CommandArgumentType.String, 13 | required = false, 14 | ), 15 | *if (fileCount == 1) { 16 | arrayOf( 17 | CommandArgumentInfo( 18 | key = "filecount", 19 | aliases = setOf("n"), 20 | description = "The number of images to send.", 21 | type = CommandArgumentType.Integer, 22 | required = false, 23 | defaultValue = 1, 24 | validator = RangeValidator(1..10), 25 | ), 26 | ) 27 | } else { 28 | emptyArray() 29 | }, 30 | inputRequirement = InputRequirement.NONE, 31 | ) { 32 | 33 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask { 34 | val tags = arguments.getStringOrEmpty("tags") 35 | val fileCount = if (fileCount == 1) 36 | arguments.getRequired( 37 | "filecount", 38 | CommandArgumentType.Integer, 39 | ) 40 | else fileCount 41 | return createApiFilesTask(tags, fileCount, maxFileSize) 42 | } 43 | 44 | protected abstract fun createApiFilesTask(tags: String, fileCount: Int, maxFileSize: Long): FileTask 45 | } 46 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/AutoCropCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.task.AutoCropTask 4 | import io.github.shaksternano.borgar.core.task.FileTask 5 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 6 | import java.awt.Color 7 | 8 | object AutoCropCommand : FileCommand( 9 | CommandArgumentInfo( 10 | key = "tolerance", 11 | aliases = setOf("t"), 12 | description = "Background crop colour tolerance.", 13 | type = CommandArgumentType.Double, 14 | required = false, 15 | defaultValue = 0.2, 16 | validator = RangeValidator.ZERO_TO_ONE, 17 | ), 18 | CommandArgumentInfo( 19 | key = "rgb", 20 | description = "Background color to crop out. By default it is the color of the top left pixel.", 21 | type = CommandArgumentType.Integer, 22 | required = false, 23 | ), 24 | CommandArgumentInfo( 25 | key = "onlycheckfirst", 26 | aliases = setOf("first", "f"), 27 | description = "Whether to only check the background in the first frame or not.", 28 | type = CommandArgumentType.Boolean, 29 | required = false, 30 | defaultValue = false, 31 | ), 32 | ) { 33 | 34 | override val name: String = "autocrop" 35 | override val description: String = "Automatically crops out background color." 36 | 37 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask { 38 | val cropColor = arguments.getOptional("rgb", CommandArgumentType.Integer) 39 | ?.let { Color(it) } 40 | val tolerance = arguments.getRequired("tolerance", CommandArgumentType.Double) 41 | val onlyCheckFirst = arguments.getRequired("onlycheckfirst", CommandArgumentType.Boolean) 42 | return AutoCropTask(cropColor, tolerance, onlyCheckFirst, maxFileSize) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/CaptionCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.task.CaptionTask 4 | import io.github.shaksternano.borgar.core.task.FileTask 5 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 6 | import io.github.shaksternano.borgar.messaging.util.getEmojiAndUrlDrawables 7 | 8 | sealed class CaptionCommand( 9 | override val name: String, 10 | private val isCaption2: Boolean, 11 | ) : FileCommand( 12 | CommandArgumentInfo( 13 | key = "caption", 14 | description = "The caption text", 15 | type = CommandArgumentType.String, 16 | ), 17 | CommandArgumentInfo( 18 | key = "bottom", 19 | aliases = setOf("b"), 20 | description = "Whether the caption should be at the bottom instead of the top.", 21 | type = CommandArgumentType.Boolean, 22 | required = false, 23 | defaultValue = false, 24 | ), 25 | ) { 26 | 27 | object Caption : CaptionCommand( 28 | "caption", 29 | false, 30 | ) 31 | 32 | object Caption2 : CaptionCommand( 33 | "caption2", 34 | true, 35 | ) 36 | 37 | override val description: String = 38 | "Captions a media file." 39 | 40 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask { 41 | val caption = arguments.getDefaultStringOrEmpty() 42 | val bottom = arguments.getRequired("bottom", CommandArgumentType.Boolean) 43 | val messageIntersection = event.asMessageIntersection(arguments) 44 | return CaptionTask( 45 | caption = formatMentions(caption, messageIntersection), 46 | isCaption2 = isCaption2, 47 | isBottom = bottom, 48 | nonTextParts = messageIntersection.getEmojiAndUrlDrawables(), 49 | maxFileSize = maxFileSize, 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/CatCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.task.CatTask 4 | import io.github.shaksternano.borgar.core.task.FileTask 5 | 6 | class CatCommand( 7 | override val name: String, 8 | override val description: String, 9 | fileCount: Int, 10 | ) : ApiFilesCommand( 11 | fileCount, 12 | ) { 13 | 14 | companion object { 15 | val CAT: Command = CatCommand( 16 | name = "cat", 17 | description = "Sends a random cat image.", 18 | fileCount = 1, 19 | ) 20 | 21 | val CAT_BOMB: Command = CatCommand( 22 | name = "catbomb", 23 | description = "Sends a bunch of random cat images.", 24 | fileCount = 10, 25 | ) 26 | } 27 | 28 | override fun createApiFilesTask(tags: String, fileCount: Int, maxFileSize: Long): FileTask = 29 | CatTask(tags, fileCount, maxFileSize) 30 | } 31 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/CommandArgumentInfo.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.util.kClass 4 | 5 | data class CommandArgumentInfo( 6 | val key: String, 7 | val aliases: Set = emptySet(), 8 | val description: String = "", 9 | val type: CommandArgumentType, 10 | val required: Boolean = true, 11 | val defaultValue: T? = null, 12 | val validator: Validator = allowAllValidator(), 13 | val autoCompleteHandler: CommandAutoCompleteHandler? = null, 14 | ) { 15 | 16 | override fun equals(other: Any?): Boolean { 17 | if (this === other) return true 18 | if (kClass != other?.kClass) return false 19 | other as CommandArgumentInfo<*> 20 | return key == other.key 21 | } 22 | 23 | override fun hashCode(): Int = key.hashCode() 24 | } 25 | 26 | val CommandArgumentInfo<*>.keyWithPrefix: String 27 | get() = ARGUMENT_PREFIX + key 28 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/CommandArguments.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.messaging.entity.Attachment 4 | 5 | interface CommandArguments { 6 | 7 | val defaultKey: String? 8 | val typedForm: String 9 | 10 | operator fun contains(key: String): Boolean 11 | 12 | operator fun get(key: String, argumentType: SimpleCommandArgumentType): T? 13 | 14 | suspend fun getSuspend(key: String, argumentType: CommandArgumentType): T? 15 | } 16 | 17 | fun CommandArguments.getDefaultStringOrEmpty(): String = 18 | defaultKey?.let { getStringOrEmpty(it) } ?: "" 19 | 20 | fun CommandArguments.getStringOrEmpty(key: String): String = 21 | this[key, CommandArgumentType.String] ?: "" 22 | 23 | fun CommandArguments.getDefaultAttachment(): Attachment? = 24 | this[FILE_ARGUMENT_INFO.key, CommandArgumentType.Attachment] 25 | 26 | fun CommandArguments.getDefaultUrl(): String? = 27 | this[URL_ARGUMENT_INFO.key, CommandArgumentType.String] 28 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/CommandAutoCompleteHandler.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.messaging.BotManager 4 | 5 | sealed interface CommandAutoCompleteHandler { 6 | 7 | suspend fun handleAutoComplete( 8 | command: kotlin.String, 9 | argument: kotlin.String, 10 | currentValue: T, 11 | manager: BotManager, 12 | ): List 13 | 14 | fun interface Long : CommandAutoCompleteHandler 15 | 16 | fun interface Double : CommandAutoCompleteHandler 17 | 18 | fun interface String : CommandAutoCompleteHandler 19 | } 20 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/CommandMessageIntersection.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.messaging.entity.* 4 | import io.github.shaksternano.borgar.messaging.entity.channel.Channel 5 | import io.github.shaksternano.borgar.messaging.entity.channel.MessageChannel 6 | import kotlinx.coroutines.flow.Flow 7 | import kotlinx.coroutines.flow.emptyFlow 8 | 9 | interface CommandMessageIntersection : Entity { 10 | 11 | val authorId: String 12 | val content: String 13 | val attachments: List 14 | val customEmojis: Flow 15 | val stickers: Flow 16 | val referencedMessages: Flow 17 | val mentionedUsers: Flow 18 | val mentionedChannels: Flow 19 | val mentionedRoles: Flow 20 | 21 | suspend fun getAuthor(): User 22 | 23 | suspend fun getAuthorMember(): Member? 24 | 25 | suspend fun getChannel(): MessageChannel? 26 | 27 | suspend fun getGuild(): Guild? 28 | 29 | suspend fun getGroup(): Group? 30 | 31 | suspend fun getPreviousMessages(): Flow = 32 | getChannel()?.getPreviousMessages(id) ?: emptyFlow() 33 | 34 | suspend fun getEmbeds(): List 35 | } 36 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/CommandResponse.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.io.DataSource 4 | 5 | data class CommandResponse( 6 | val content: String = "", 7 | val files: List = emptyList(), 8 | val suppressEmbeds: Boolean = false, 9 | val responseData: Any? = null, 10 | ) 11 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/CutoutSpeechBubbleCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.task.FileTask 4 | import io.github.shaksternano.borgar.core.task.SpeechBubbleTask 5 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 6 | 7 | object CutoutSpeechBubbleCommand : FileCommand( 8 | SPEECH_BUBBLE_FLIP_ARGUMENT, 9 | CommandArgumentInfo( 10 | key = "opaque", 11 | aliases = setOf("o"), 12 | description = "Whether to make the speech bubble opaque or not.", 13 | type = CommandArgumentType.Boolean, 14 | required = false, 15 | defaultValue = false, 16 | ), 17 | ) { 18 | 19 | override val name: String = "cutoutspeechbubble" 20 | override val aliases: Set = setOf("sbi") 21 | override val description: String = "Cuts out a speech bubble from media." 22 | 23 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask { 24 | val flipped = arguments.getRequired("flip", CommandArgumentType.Boolean) 25 | val opaque = arguments.getRequired("opaque", CommandArgumentType.Boolean) 26 | return SpeechBubbleTask( 27 | cutout = true, 28 | flipped = flipped, 29 | opaque = opaque, 30 | maxFileSize = maxFileSize, 31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/DeleteTemplateCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.data.repository.TemplateRepository 4 | import io.github.shaksternano.borgar.core.util.asSingletonList 5 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 6 | 7 | object DeleteTemplateCommand : NonChainableCommand() { 8 | 9 | override val name: String = "removetemplate" 10 | override val description: String = "Deletes a custom image template for this server or DM." 11 | override val argumentInfo: Set> = setOf( 12 | CommandArgumentInfo( 13 | key = "template", 14 | description = "The name of the template to delete.", 15 | type = CommandArgumentType.String, 16 | required = true, 17 | ) 18 | ) 19 | override val requiredPermissions: Set = setOf(Permission.MANAGE_GUILD_EXPRESSIONS) 20 | override val deferReply: Boolean = true 21 | override val ephemeralReply: Boolean = true 22 | 23 | override suspend fun run(arguments: CommandArguments, event: CommandEvent): List { 24 | val commandName = arguments.getRequired("template", CommandArgumentType.String).lowercase() 25 | val guild = event.getGuild() 26 | val entityId = guild?.id ?: event.getAuthor().id 27 | if (!TemplateRepository.exists(commandName, entityId)) { 28 | return CommandResponse("No template with the command name **$commandName** exists!").asSingletonList() 29 | } 30 | TemplateRepository.delete(commandName, entityId) 31 | HelpCommand.removeCachedMessage(entityId) 32 | guild?.deleteCommand(commandName) 33 | return CommandResponse("Template **$commandName** deleted!").asSingletonList() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/DemotivateCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.task.DemotivateTask 4 | import io.github.shaksternano.borgar.core.task.FileTask 5 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 6 | import io.github.shaksternano.borgar.messaging.exception.MissingArgumentException 7 | import io.github.shaksternano.borgar.messaging.util.getEmojiAndUrlDrawables 8 | 9 | object DemotivateCommand : FileCommand( 10 | CommandArgumentInfo( 11 | key = "text", 12 | description = "The text to put on the image", 13 | type = CommandArgumentType.String, 14 | required = false, 15 | ), 16 | CommandArgumentInfo( 17 | key = "subtext", 18 | aliases = setOf("sub"), 19 | description = "The subtext to put on the image", 20 | type = CommandArgumentType.String, 21 | required = false, 22 | ) 23 | ) { 24 | 25 | override val name: String = "demotiv" 26 | override val description: String = "Puts image in demotivate meme." 27 | 28 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask { 29 | val text = arguments.getDefaultStringOrEmpty() 30 | val subText = arguments.getStringOrEmpty("subtext") 31 | if (text.isBlank() && subText.isBlank()) { 32 | throw MissingArgumentException("No text was provided.") 33 | } 34 | val messageIntersection = event.asMessageIntersection(arguments) 35 | return DemotivateTask( 36 | text = formatMentions(text, messageIntersection), 37 | subText = formatMentions(subText, messageIntersection), 38 | nonTextParts = messageIntersection.getEmojiAndUrlDrawables(), 39 | maxFileSize = maxFileSize, 40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/DownloadCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.exception.ErrorResponseException 4 | import io.github.shaksternano.borgar.core.task.DownloadTask 5 | import io.github.shaksternano.borgar.core.task.FileTask 6 | import io.github.shaksternano.borgar.core.util.getUrls 7 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 8 | import io.github.shaksternano.borgar.messaging.util.searchExceptSelf 9 | 10 | object DownloadCommand : FileCommand( 11 | CommandArgumentInfo( 12 | key = "url", 13 | description = "The URL to download from.", 14 | type = CommandArgumentType.String, 15 | required = true, 16 | ), 17 | CommandArgumentInfo( 18 | key = "audioonly", 19 | aliases = setOf("a"), 20 | description = "Whether to only download audio or not. Not all websites support this.", 21 | type = CommandArgumentType.Boolean, 22 | required = false, 23 | defaultValue = false, 24 | ), 25 | CommandArgumentInfo( 26 | key = "filenumber", 27 | aliases = setOf("n"), 28 | description = "The file to download. If not specified, all files will be downloaded.", 29 | type = CommandArgumentType.Integer, 30 | required = false, 31 | validator = PositiveIntValidator, 32 | ), 33 | inputRequirement = InputRequirement.NONE, 34 | ) { 35 | 36 | override val name: String = "download" 37 | override val aliases: Set = setOf("dl") 38 | override val description: String = 39 | "Downloads a file from a social media website, for example, a video from YouTube." 40 | 41 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask { 42 | val url = arguments.getOptional("url", CommandArgumentType.String) 43 | ?: event.asMessageIntersection(arguments).searchExceptSelf { 44 | it.content.getUrls().firstOrNull() 45 | } 46 | ?: throw ErrorResponseException("No URL specified!") 47 | val audioOnly = arguments.getRequired("audioonly", CommandArgumentType.Boolean) 48 | val fileNumber = arguments.getOptional("filenumber", CommandArgumentType.Integer) 49 | return DownloadTask(url, audioOnly, fileNumber, maxFileSize) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/EmojiImageCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.task.FileTask 4 | import io.github.shaksternano.borgar.core.task.UrlFileTask 5 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 6 | import io.github.shaksternano.borgar.messaging.util.getEmojiUrls 7 | import io.github.shaksternano.borgar.messaging.util.searchOrThrow 8 | 9 | object EmojiImageCommand : FileCommand( 10 | CommandArgumentInfo( 11 | key = "emoji", 12 | description = "The emoji to get the image of.", 13 | type = CommandArgumentType.String, 14 | required = false, 15 | ), 16 | inputRequirement = InputRequirement.NONE, 17 | ) { 18 | 19 | override val name: String = "emojiimage" 20 | override val aliases: Set = setOf("emoji") 21 | override val description: String = "Gets the image of an emoji." 22 | 23 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask { 24 | val messageIntersection = event.asMessageIntersection(arguments) 25 | val emojis = arguments.getDefaultStringOrEmpty() 26 | val emojiUrls = messageIntersection.searchOrThrow("No emojis found.") { message -> 27 | val urls = message.getEmojiUrls() 28 | val filteredUrls = if (message == messageIntersection) { 29 | urls.filter { (mention, _) -> 30 | emojis.contains(mention) 31 | } 32 | } else { 33 | urls 34 | } 35 | filteredUrls.values.ifEmpty { 36 | null 37 | } 38 | } 39 | return UrlFileTask(emojiUrls) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/FlipCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.task.FileTask 4 | import io.github.shaksternano.borgar.core.task.FlipTask 5 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 6 | 7 | object FlipCommand : FileCommand( 8 | CommandArgumentInfo( 9 | key = "vertical", 10 | aliases = setOf("v"), 11 | description = "Whether to flip vertically or not.", 12 | type = CommandArgumentType.Boolean, 13 | required = false, 14 | defaultValue = false, 15 | ), 16 | ) { 17 | 18 | override val name: String = "flip" 19 | override val description: String = "Flips media horizontally or vertically." 20 | 21 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask { 22 | val vertical = arguments.getRequired("vertical", CommandArgumentType.Boolean) 23 | return FlipTask(vertical, maxFileSize) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/GifCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.task.FileTask 4 | import io.github.shaksternano.borgar.core.task.GifTask 5 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 6 | 7 | object GifCommand : FileCommand( 8 | CommandArgumentInfo( 9 | key = "transcode", 10 | aliases = setOf("t"), 11 | description = "Forces transcoding to GIF instead of changing the file extension to .gif.", 12 | type = CommandArgumentType.Boolean, 13 | required = false, 14 | defaultValue = false, 15 | ), 16 | CommandArgumentInfo( 17 | key = "changeextension", 18 | aliases = setOf("e"), 19 | description = "Forces changing the file extension to .gif instead of transcoding to GIF.", 20 | type = CommandArgumentType.Boolean, 21 | required = false, 22 | defaultValue = false, 23 | ), 24 | ) { 25 | 26 | override val name: String = "gif" 27 | override val description: String = "Converts media to a GIF file. " + 28 | "Static images will just have their file extension changed to .gif." 29 | 30 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask { 31 | val forceTranscode = arguments.getRequired("transcode", CommandArgumentType.Boolean) 32 | val forceRename = arguments.getRequired("changeextension", CommandArgumentType.Boolean) 33 | return GifTask( 34 | forceTranscode = forceTranscode, 35 | forceRename = forceRename, 36 | maxFileSize = maxFileSize, 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/GuildBannerCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.exception.ErrorResponseException 4 | import io.github.shaksternano.borgar.core.task.FileTask 5 | import io.github.shaksternano.borgar.core.task.UrlFileTask 6 | import io.github.shaksternano.borgar.core.util.ChannelEnvironment 7 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 8 | 9 | object GuildBannerCommand : FileCommand( 10 | inputRequirement = InputRequirement.NONE, 11 | ) { 12 | 13 | override val name: String = "serverbanner" 14 | override val description: String = "Gets the banner image of this server." 15 | override val environment: Set = setOf(ChannelEnvironment.GUILD) 16 | 17 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask { 18 | val guild = event.getGuild() ?: throw IllegalStateException("Command run outside of a guild") 19 | val bannerUrl = guild.bannerUrl ?: throw ErrorResponseException("This server has no banner image.") 20 | return UrlFileTask(bannerUrl) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/GuildCountCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.util.asSingletonList 4 | import io.github.shaksternano.borgar.messaging.BOT_MANAGERS 5 | import io.github.shaksternano.borgar.messaging.MessagingPlatform 6 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 7 | 8 | private val MESSAGING_PLATFORM_TYPE: CommandArgumentType = 9 | CommandArgumentType.Enum(MessagingPlatform::class, "messaging platform") 10 | 11 | object GuildCountCommand : NonChainableCommand() { 12 | 13 | override val name: String = "servercount" 14 | override val aliases: Set = setOf("servers") 15 | override val description: String = "Gets the number of servers that this bot is in." 16 | override val argumentInfo: Set> = setOf( 17 | CommandArgumentInfo( 18 | key = "platform", 19 | description = "The messaging platform to get the server count for.", 20 | type = MESSAGING_PLATFORM_TYPE, 21 | required = false, 22 | ), 23 | ) 24 | 25 | override suspend fun run(arguments: CommandArguments, event: CommandEvent): List { 26 | val platform = arguments.getOptional("platform", MESSAGING_PLATFORM_TYPE) 27 | val managers = 28 | if (platform == null) BOT_MANAGERS 29 | else BOT_MANAGERS.filter { 30 | it.platform == platform 31 | } 32 | val guildCount = managers.sumOf { 33 | it.getGuildCount() 34 | } 35 | var message = "This bot is in $guildCount " 36 | if (platform != null) { 37 | message += "${platform.displayName} " 38 | } 39 | message += "server" 40 | if (guildCount != 1) { 41 | message += "s" 42 | } 43 | message += "." 44 | return CommandResponse(message).asSingletonList() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/GuildIconCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.exception.ErrorResponseException 4 | import io.github.shaksternano.borgar.core.task.FileTask 5 | import io.github.shaksternano.borgar.core.task.UrlFileTask 6 | import io.github.shaksternano.borgar.core.util.ChannelEnvironment 7 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 8 | 9 | object GuildIconCommand : FileCommand( 10 | inputRequirement = InputRequirement.NONE, 11 | ) { 12 | 13 | override val name: String = "servericon" 14 | override val aliases: Set = setOf("icon") 15 | override val description: String = "Gets the icon of this server." 16 | override val environment: Set = setOf( 17 | ChannelEnvironment.GUILD, 18 | ChannelEnvironment.GROUP, 19 | ) 20 | 21 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask { 22 | val chatroom = event.getGuild() 23 | ?: event.getGroup() 24 | ?: throw IllegalStateException("Command run outside of a guild") 25 | val iconUrl = chatroom.iconUrl ?: throw ErrorResponseException("This server has no icon.") 26 | return UrlFileTask(iconUrl) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/GuildSplashCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.exception.ErrorResponseException 4 | import io.github.shaksternano.borgar.core.task.FileTask 5 | import io.github.shaksternano.borgar.core.task.UrlFileTask 6 | import io.github.shaksternano.borgar.core.util.ChannelEnvironment 7 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 8 | 9 | object GuildSplashCommand : FileCommand( 10 | inputRequirement = InputRequirement.NONE, 11 | ) { 12 | 13 | override val name: String = "serversplash" 14 | override val aliases: Set = setOf("splash") 15 | override val description: String = "Gets the splash image of this server." 16 | override val environment: Set = setOf(ChannelEnvironment.GUILD) 17 | 18 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask { 19 | val guild = event.getGuild() ?: throw IllegalStateException("Command run outside of a guild") 20 | val splashUrl = guild.splashUrl ?: throw ErrorResponseException("This server has no splash image.") 21 | return UrlFileTask(splashUrl) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/InvertColorsCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.task.FileTask 4 | import io.github.shaksternano.borgar.core.task.InvertColorsTask 5 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 6 | 7 | object InvertColorsCommand : FileCommand() { 8 | 9 | override val name: String = "invert" 10 | override val description: String = "Inverts the colors of images." 11 | 12 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask = 13 | InvertColorsTask(maxFileSize) 14 | } 15 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/LiveReactionCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.task.FileTask 4 | import io.github.shaksternano.borgar.core.task.LiveReactionTask 5 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 6 | 7 | object LiveReactionCommand : FileCommand() { 8 | 9 | override val name: String = "livereaction" 10 | override val aliases: Set = setOf("live") 11 | override val description: String = "Live reaction meme." 12 | 13 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask = 14 | LiveReactionTask(maxFileSize) 15 | } 16 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/LoopCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.task.FileTask 4 | import io.github.shaksternano.borgar.core.task.LoopTask 5 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 6 | 7 | object LoopCommand : FileCommand( 8 | CommandArgumentInfo( 9 | key = "loopcount", 10 | aliases = setOf("n"), 11 | description = "The number of times to loop the GIF." 12 | + " A value of -1 will make it loop forever.", 13 | type = CommandArgumentType.Integer, 14 | required = true, 15 | validator = RangeValidator(-1..65535), 16 | ), 17 | ) { 18 | 19 | override val name: String = "loop" 20 | override val description: String = "Changes the number of times a GIF loops." 21 | 22 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask { 23 | val loopCount = arguments.getRequired("loopcount", CommandArgumentType.Integer) 24 | return LoopTask(loopCount) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/MemeCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.task.FileTask 4 | import io.github.shaksternano.borgar.core.task.MemeTask 5 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 6 | import io.github.shaksternano.borgar.messaging.exception.MissingArgumentException 7 | import io.github.shaksternano.borgar.messaging.util.getEmojiAndUrlDrawables 8 | 9 | object MemeCommand : FileCommand( 10 | CommandArgumentInfo( 11 | key = "text", 12 | description = "The text to put on the top of the image.", 13 | type = CommandArgumentType.String, 14 | required = false, 15 | ), 16 | CommandArgumentInfo( 17 | key = "bottom", 18 | description = "The text to put on the bottom of the image.", 19 | type = CommandArgumentType.String, 20 | required = false, 21 | ), 22 | ) { 23 | 24 | override val name: String = "meme" 25 | override val description: String = "Adds impact font text to a media file." 26 | 27 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask { 28 | val topText = arguments.getDefaultStringOrEmpty() 29 | val bottomText = arguments.getStringOrEmpty("bottom") 30 | if (topText.isBlank() && bottomText.isBlank()) { 31 | throw MissingArgumentException("No text was provided.") 32 | } 33 | val messageIntersection = event.asMessageIntersection(arguments) 34 | return MemeTask( 35 | topText = formatMentions(topText, messageIntersection), 36 | bottomText = formatMentions(bottomText, messageIntersection), 37 | nonTextParts = messageIntersection.getEmojiAndUrlDrawables(), 38 | maxFileSize = maxFileSize, 39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/Permission.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | enum class Permission { 4 | MANAGE_GUILD_EXPRESSIONS, 5 | } 6 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/PingCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.util.asSingletonList 4 | import io.github.shaksternano.borgar.messaging.entity.Message 5 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 6 | import java.time.temporal.ChronoUnit 7 | 8 | object PingCommand : NonChainableCommand() { 9 | 10 | override val name: String = "ping" 11 | override val description: String = "Checks the bot's latency." 12 | 13 | override suspend fun run(arguments: CommandArguments, event: CommandEvent): List = 14 | CommandResponse("Ping: ...").asSingletonList() 15 | 16 | override suspend fun onResponseSend( 17 | response: CommandResponse, 18 | responseNumber: Int, 19 | responseCount: Int, 20 | sent: Message, 21 | event: CommandEvent, 22 | ) { 23 | val ping = event.timeCreated.until(sent.timeCreated, ChronoUnit.MILLIS) 24 | sent.edit("Ping: ${ping}ms") 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/PixelateCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.task.FileTask 4 | import io.github.shaksternano.borgar.core.task.PixelateTask 5 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 6 | 7 | object PixelateCommand : FileCommand( 8 | CommandArgumentInfo( 9 | key = "pixelationmultiplier", 10 | aliases = setOf("pm"), 11 | description = "Pixelation multiplier.", 12 | type = CommandArgumentType.Double, 13 | validator = GreaterThanOneValidator, 14 | ), 15 | ) { 16 | 17 | override val name: String = "pixelate" 18 | override val aliases: Set = setOf("pixel") 19 | override val description: String = "Pixelates media." 20 | 21 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask { 22 | val pixelationMultiplier = arguments.getRequired("pixelationmultiplier", CommandArgumentType.Double) 23 | return PixelateTask(pixelationMultiplier, maxFileSize) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/ReduceFpsCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.task.FileTask 4 | import io.github.shaksternano.borgar.core.task.ReduceFpsTask 5 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 6 | 7 | object ReduceFpsCommand : FileCommand( 8 | CommandArgumentInfo( 9 | key = "fpsreductionratio", 10 | aliases = setOf("ratio"), 11 | description = "FPS reduction multiplier.", 12 | type = CommandArgumentType.Double, 13 | required = false, 14 | defaultValue = 2.0, 15 | validator = GreaterThanOneValidator, 16 | ) 17 | ) { 18 | 19 | override val name: String = "reducefps" 20 | override val aliases: Set = setOf("redfps") 21 | override val description: String = "Reduces the FPS of a media file." 22 | 23 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask { 24 | val fpsReductionRatio = arguments.getRequired("fpsreductionratio", CommandArgumentType.Double) 25 | return ReduceFpsTask(fpsReductionRatio, maxFileSize) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/ResizeCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.task.FileTask 4 | import io.github.shaksternano.borgar.core.task.StretchTask 5 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 6 | 7 | object ResizeCommand : FileCommand( 8 | CommandArgumentInfo( 9 | key = "resizemultiplier", 10 | aliases = setOf("r", "resize"), 11 | description = "Resize multiplier.", 12 | type = CommandArgumentType.Double, 13 | validator = ResizeValidator, 14 | ), 15 | CommandArgumentInfo( 16 | key = "raw", 17 | description = "Whether to stretch without extra processing.", 18 | type = CommandArgumentType.Boolean, 19 | required = false, 20 | defaultValue = false, 21 | ), 22 | ) { 23 | 24 | override val name: String = "resize" 25 | override val description: String = "Resizes media." 26 | 27 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask { 28 | val resizeMultiplier = arguments.getRequired("resizemultiplier", CommandArgumentType.Double) 29 | val raw = arguments.getRequired("raw", CommandArgumentType.Boolean) 30 | return StretchTask( 31 | widthMultiplier = resizeMultiplier, 32 | heightMultiplier = resizeMultiplier, 33 | raw = raw, 34 | maxFileSize = maxFileSize, 35 | ) 36 | } 37 | } 38 | 39 | private object ResizeValidator : MinValueValidator( 40 | minValue = 0.0, 41 | ) { 42 | 43 | override fun validate(value: Double): Boolean = value != 1.0 && value > 0.0 44 | 45 | override fun errorMessage(value: Double, key: String): String = 46 | if (value == 1.0) { 47 | "The argument **$key** must not be 1." 48 | } else { 49 | "The argument **$key** must be positive." 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/ReverseCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.task.FileTask 4 | import io.github.shaksternano.borgar.core.task.SpeedTask 5 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 6 | 7 | object ReverseCommand : FileCommand() { 8 | 9 | override val name: String = "reverse" 10 | override val description: String = "Reverses animated media." 11 | 12 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask = 13 | SpeedTask( 14 | speed = -1.0, 15 | maxFileSize = maxFileSize, 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/RotateCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.task.FileTask 4 | import io.github.shaksternano.borgar.core.task.RotateTask 5 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 6 | import java.awt.Color 7 | 8 | object RotateCommand : FileCommand( 9 | CommandArgumentInfo( 10 | key = "degrees", 11 | description = "Degrees to rotate by.", 12 | type = CommandArgumentType.Double, 13 | defaultValue = 90.0, 14 | required = false, 15 | ), 16 | CommandArgumentInfo( 17 | key = "backgroundrgb", 18 | aliases = setOf("background", "bg"), 19 | description = "Background RGB color to fill in the empty space. By default it is transparent.", 20 | type = CommandArgumentType.Integer, 21 | required = false, 22 | ), 23 | ) { 24 | 25 | override val name: String = "rotate" 26 | override val description: String = "Rotates media." 27 | 28 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask { 29 | val degrees = arguments.getRequired("degrees", CommandArgumentType.Double) 30 | val backgroundRgb = arguments.getOptional("backgroundrgb", CommandArgumentType.Integer) 31 | val backgroundColor = backgroundRgb?.let { Color(it) } 32 | return RotateTask(degrees, backgroundColor, maxFileSize) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/ShutdownCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.logger 4 | import io.github.shaksternano.borgar.core.util.asSingletonList 5 | import io.github.shaksternano.borgar.messaging.entity.Message 6 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 7 | import kotlinx.coroutines.DelicateCoroutinesApi 8 | import kotlinx.coroutines.GlobalScope 9 | import kotlinx.coroutines.delay 10 | import kotlinx.coroutines.launch 11 | import kotlin.system.exitProcess 12 | 13 | object ShutdownCommand : NonChainableCommand() { 14 | 15 | override val name: String = "shutdown" 16 | override val description: String = "Shuts down the bot. Only the bot owner can use this command." 17 | 18 | override suspend fun run(arguments: CommandArguments, event: CommandEvent): List = 19 | if (event.getAuthor().id == event.manager.ownerId) { 20 | CommandResponse( 21 | content = "Shutting down...", 22 | responseData = true, 23 | ).also { 24 | // In case the response fails to send 25 | @OptIn(DelicateCoroutinesApi::class) 26 | GlobalScope.launch { 27 | delay(5000) 28 | shutdown() 29 | } 30 | } 31 | } else { 32 | CommandResponse("You don't have permission to use this command.") 33 | }.asSingletonList() 34 | 35 | override suspend fun onResponseSend( 36 | response: CommandResponse, 37 | responseNumber: Int, 38 | responseCount: Int, 39 | sent: Message, 40 | event: CommandEvent, 41 | ) { 42 | if (response.responseData == true) { 43 | shutdown() 44 | } 45 | } 46 | 47 | private fun shutdown() { 48 | logger.info("Shutdown request received, shutting down...") 49 | exitProcess(0) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/SpeechBubbleCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.task.FileTask 4 | import io.github.shaksternano.borgar.core.task.SpeechBubbleTask 5 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 6 | 7 | val SPEECH_BUBBLE_FLIP_ARGUMENT: CommandArgumentInfo<*> = CommandArgumentInfo( 8 | key = "flip", 9 | aliases = setOf("f"), 10 | description = "Whether to flip the speech bubble or not.", 11 | type = CommandArgumentType.Boolean, 12 | required = false, 13 | defaultValue = false, 14 | ) 15 | 16 | object SpeechBubbleCommand : FileCommand( 17 | SPEECH_BUBBLE_FLIP_ARGUMENT, 18 | ) { 19 | 20 | override val name: String = "speechbubble" 21 | override val aliases: Set = setOf("sb") 22 | override val description: String = "Overlays a speech bubble over media." 23 | 24 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask { 25 | val flipped = arguments.getRequired("flip", CommandArgumentType.Boolean) 26 | return SpeechBubbleTask( 27 | cutout = false, 28 | flipped = flipped, 29 | opaque = false, 30 | maxFileSize = maxFileSize, 31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/SpeedCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.task.FileTask 4 | import io.github.shaksternano.borgar.core.task.SpeedTask 5 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 6 | 7 | object SpeedCommand : FileCommand( 8 | CommandArgumentInfo( 9 | key = "multiplier", 10 | aliases = setOf("speed"), 11 | description = "Speed multiplier.", 12 | type = CommandArgumentType.Double, 13 | required = false, 14 | defaultValue = 2.0, 15 | validator = SpeedValidator, 16 | ), 17 | ) { 18 | 19 | override val name: String = "speed" 20 | override val description: String = "Speeds up or slows down animated media." 21 | 22 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask { 23 | val speedMultiplier = arguments.getRequired("multiplier", CommandArgumentType.Double) 24 | return SpeedTask(speedMultiplier, maxFileSize) 25 | } 26 | } 27 | 28 | private object SpeedValidator : Validator { 29 | 30 | override val description: String = "Must not be 0 or 1." 31 | 32 | override fun validate(value: Double): Boolean = 33 | value != 0.0 && value != 1.0 34 | 35 | override fun errorMessage(value: Double, key: String): String = 36 | if (value == 0.0) "Speed multiplier cannot be 0." 37 | else "Speed multiplier cannot be 1." 38 | } 39 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/SpinCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.task.FileTask 4 | import io.github.shaksternano.borgar.core.task.SpinTask 5 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 6 | import java.awt.Color 7 | 8 | object SpinCommand : FileCommand( 9 | CommandArgumentInfo( 10 | key = "speed", 11 | description = "The spin speed.", 12 | type = CommandArgumentType.Double, 13 | required = false, 14 | defaultValue = 1.0 15 | ), 16 | CommandArgumentInfo( 17 | key = "backgroundrgb", 18 | aliases = setOf("background", "bg"), 19 | description = "Background RGB color to fill in the empty space. By default it is transparent.", 20 | type = CommandArgumentType.Integer, 21 | required = false, 22 | ), 23 | ) { 24 | 25 | override val name: String = "spin" 26 | override val description: String = "Spins a media file." 27 | 28 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask { 29 | val spinSpeed = arguments.getRequired("speed", CommandArgumentType.Double) 30 | val backgroundRgb = arguments.getOptional("backgroundrgb", CommandArgumentType.Integer) 31 | val backgroundColor = backgroundRgb?.let { Color(it) } 32 | return SpinTask(spinSpeed, backgroundColor, maxFileSize) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/StickerImageCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.task.FileTask 4 | import io.github.shaksternano.borgar.core.task.UrlFileTask 5 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 6 | import io.github.shaksternano.borgar.messaging.util.searchOrThrow 7 | import kotlinx.coroutines.flow.map 8 | import kotlinx.coroutines.flow.toSet 9 | 10 | object StickerImageCommand : FileCommand( 11 | inputRequirement = InputRequirement.NONE, 12 | ) { 13 | 14 | override val name: String = "stickerimage" 15 | override val aliases: Set = setOf("sticker") 16 | override val description: String = "Gets the image of a sticker." 17 | 18 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask { 19 | val stickerUrls = event.asMessageIntersection(arguments) 20 | .searchOrThrow("No stickers found.") { message -> 21 | message.stickers 22 | .map { it.imageUrl } 23 | .toSet() 24 | .ifEmpty { 25 | null 26 | } 27 | } 28 | return UrlFileTask(stickerUrls) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/StretchCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.task.FileTask 4 | import io.github.shaksternano.borgar.core.task.StretchTask 5 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 6 | 7 | object StretchCommand : FileCommand( 8 | CommandArgumentInfo( 9 | key = "widthmultiplier", 10 | aliases = setOf("w", "width"), 11 | description = "Width stretch multiplier.", 12 | type = CommandArgumentType.Double, 13 | required = false, 14 | defaultValue = 2.0, 15 | validator = PositiveDoubleValidator, 16 | ), 17 | CommandArgumentInfo( 18 | key = "heightmultiplier", 19 | aliases = setOf("h", "height"), 20 | description = "Height stretch multiplier.", 21 | type = CommandArgumentType.Double, 22 | required = false, 23 | defaultValue = 1.0, 24 | validator = PositiveDoubleValidator, 25 | ), 26 | CommandArgumentInfo( 27 | key = "raw", 28 | description = "Whether to stretch without extra processing.", 29 | type = CommandArgumentType.Boolean, 30 | required = false, 31 | defaultValue = false, 32 | ), 33 | ) { 34 | 35 | override val name: String = "stretch" 36 | override val description: String = "Stretches media." 37 | 38 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask { 39 | val widthMultiplier = arguments.getRequired("widthmultiplier", CommandArgumentType.Double) 40 | val heightMultiplier = arguments.getRequired("heightmultiplier", CommandArgumentType.Double) 41 | val raw = arguments.getRequired("raw", CommandArgumentType.Boolean) 42 | return StretchTask( 43 | widthMultiplier = widthMultiplier, 44 | heightMultiplier = heightMultiplier, 45 | raw = raw, 46 | maxFileSize = maxFileSize, 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/SubwaySurfersCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.task.FileTask 4 | import io.github.shaksternano.borgar.core.task.SubwaySurfersTask 5 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 6 | 7 | object SubwaySurfersCommand : FileCommand() { 8 | 9 | override val name: String = "subwaysurfers" 10 | override val aliases: Set = setOf("subway") 11 | override val description: String = "Adds Subway Surfers gameplay to media." 12 | 13 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask = 14 | SubwaySurfersTask(maxFileSize) 15 | } 16 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/TranscodeCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.task.FileTask 4 | import io.github.shaksternano.borgar.core.task.TranscodeTask 5 | import io.github.shaksternano.borgar.core.util.startsWithVowel 6 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 7 | 8 | class TranscodeCommand( 9 | private val format: String, 10 | ) : FileCommand() { 11 | 12 | companion object { 13 | val PNG: Command = TranscodeCommand("png") 14 | val JPG: Command = TranscodeCommand("jpg") 15 | val MP4: Command = TranscodeCommand("mp4") 16 | val ICO: Command = TranscodeCommand("ico") 17 | } 18 | 19 | override val name: String = format 20 | override val description: String = run { 21 | var transcodeDescription = "Converts media to a" 22 | if (format.startsWithVowel()) { 23 | transcodeDescription += "n" 24 | } 25 | transcodeDescription + " ${format.uppercase()} file." 26 | } 27 | 28 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask = 29 | TranscodeTask(format, maxFileSize) 30 | } 31 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/UncaptionCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.task.FileTask 4 | import io.github.shaksternano.borgar.core.task.UncaptionTask 5 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 6 | 7 | object UncaptionCommand : FileCommand( 8 | CommandArgumentInfo( 9 | key = "onlycheckfirst", 10 | aliases = setOf("first", "f"), 11 | description = "Whether to only check for a caption in the first frame or not.", 12 | type = CommandArgumentType.Boolean, 13 | required = false, 14 | defaultValue = false, 15 | ), 16 | ) { 17 | 18 | override val name: String = "uncaption" 19 | override val description: String = "Removes the caption from media." 20 | 21 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask { 22 | val onlyCheckFirst = arguments.getRequired("onlycheckfirst", CommandArgumentType.Boolean) 23 | return UncaptionTask(onlyCheckFirst, maxFileSize) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/UptimeCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.START_TIME 4 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 5 | import kotlin.time.TimeSource 6 | 7 | object UptimeCommand : NonChainableCommand() { 8 | 9 | override val name: String = "uptime" 10 | override val description: String = "Gets the uptime of this bot." 11 | 12 | override suspend fun run(arguments: CommandArguments, event: CommandEvent): List { 13 | val time = TimeSource.Monotonic.markNow() 14 | val uptime = time - START_TIME 15 | val uptimeString = uptime.toComponents { days, hours, minutes, seconds, _ -> 16 | if (days > 0) { 17 | if (days == 1L) { 18 | "1 day" 19 | } else { 20 | "$days days" 21 | } 22 | } else if (hours > 0) { 23 | if (hours == 1) { 24 | "1 hour" 25 | } else { 26 | "$hours hours" 27 | } 28 | } else if (minutes > 0) { 29 | if (minutes == 1) { 30 | "1 minute" 31 | } else { 32 | "$minutes minutes" 33 | } 34 | } else { 35 | if (seconds == 1) { 36 | "1 second" 37 | } else { 38 | "$seconds seconds" 39 | } 40 | } 41 | } 42 | val message = "This bot has been up for $uptimeString." 43 | return listOf(CommandResponse(message)) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/UrlFileCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.task.FileTask 4 | import io.github.shaksternano.borgar.core.task.UrlFileTask 5 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 6 | 7 | class UrlFileCommand( 8 | override val name: String, 9 | override val description: String, 10 | url: String, 11 | ) : FileCommand( 12 | inputRequirement = InputRequirement.NONE, 13 | ) { 14 | 15 | companion object { 16 | val HAEMA: Command = UrlFileCommand( 17 | name = "haema", 18 | description = "https://modrinth.com/mod/haema", 19 | url = "https://autumn.revolt.chat/attachments/yRiB5Iu4BWNkwEKtkR1fi9YmsWIY6EzaInEXqavcIt/YOU_SHOULD_DOWNLOAD_HAEMA_NOW.gif", 20 | ) 21 | 22 | val TULIN: Command = UrlFileCommand( 23 | name = "tulin", 24 | description = "The best character in The Legend of Zelda: Breath of the Wild.", 25 | url = "https://autumn.revolt.chat/attachments/i5fZQ7mIeaBauXxtf8Vh-bM3nWSXlZ07XSVJd7cdCN/Tulin.gif", 26 | ) 27 | } 28 | 29 | override val deferReply: Boolean = false 30 | private val task: FileTask = UrlFileTask(url) 31 | 32 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask = 33 | task 34 | } 35 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/UserAvatarCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.task.FileTask 4 | import io.github.shaksternano.borgar.core.task.UrlFileTask 5 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 6 | 7 | object UserAvatarCommand : FileCommand( 8 | CommandArgumentInfo( 9 | key = "user", 10 | description = "The user to get the avatar from.", 11 | type = CommandArgumentType.User, 12 | required = false, 13 | ), 14 | CommandArgumentInfo( 15 | key = "noserver", 16 | description = "Whether to ignore the server avatar or not.", 17 | type = CommandArgumentType.Boolean, 18 | required = false, 19 | defaultValue = false, 20 | ), 21 | inputRequirement = InputRequirement.NONE, 22 | ) { 23 | 24 | override val name: String = "useravatar" 25 | override val aliases: Set = setOf("avatar") 26 | override val description: String = "Gets the avatar of a user." 27 | 28 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask { 29 | val user = getReferencedUser(arguments, event) 30 | val ignoreServer = arguments.getRequired("noserver", CommandArgumentType.Boolean) 31 | val avatarUrl = run { 32 | if (!ignoreServer) { 33 | val member = event.getGuild()?.getMember(user.id) 34 | if (member != null) { 35 | return@run member.effectiveAvatarUrl 36 | } 37 | } 38 | user.effectiveAvatarUrl 39 | } 40 | return UrlFileTask(avatarUrl) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/command/UserBannerCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.command 2 | 3 | import io.github.shaksternano.borgar.core.exception.ErrorResponseException 4 | import io.github.shaksternano.borgar.core.task.FileTask 5 | import io.github.shaksternano.borgar.core.task.UrlFileTask 6 | import io.github.shaksternano.borgar.messaging.event.CommandEvent 7 | 8 | object UserBannerCommand : FileCommand( 9 | CommandArgumentInfo( 10 | key = "user", 11 | description = "The user to get the banner from.", 12 | type = CommandArgumentType.User, 13 | required = false, 14 | ), 15 | inputRequirement = InputRequirement.NONE, 16 | ) { 17 | 18 | override val name: String = "userbanner" 19 | override val aliases: Set = setOf("banner") 20 | override val description: String = "Gets the banner of a user." 21 | 22 | override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask { 23 | val user = getReferencedUser(arguments, event) 24 | val bannerUrl = user.getBannerUrl() ?: throw ErrorResponseException("**${user.effectiveName}** has no banner.") 25 | return UrlFileTask(bannerUrl) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/entity/Attachment.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.entity 2 | 3 | import io.github.shaksternano.borgar.core.io.DataSource 4 | import io.github.shaksternano.borgar.core.io.DataSourceConvertable 5 | import io.github.shaksternano.borgar.core.io.UrlDataSource 6 | import io.github.shaksternano.borgar.messaging.BotManager 7 | 8 | data class Attachment( 9 | override val id: String, 10 | val url: String, 11 | val proxyUrl: String?, 12 | val filename: String, 13 | override val manager: BotManager, 14 | val ephemeral: Boolean = false, 15 | ) : BaseEntity(), DataSourceConvertable { 16 | 17 | override fun asDataSource(): UrlDataSource = DataSource.fromUrl(url, filename) 18 | } 19 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/entity/BaseEntity.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.entity 2 | 3 | abstract class BaseEntity : Entity { 4 | 5 | override fun equals(other: Any?): Boolean { 6 | if (this === other) return true 7 | if (other is Entity) return id == other.id 8 | return false 9 | } 10 | 11 | override fun hashCode(): Int = id.hashCode() 12 | 13 | override fun toString(): String { 14 | return "${this::class.simpleName ?: "Entity"}(id='$id')" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/entity/ChatRoom.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.entity 2 | 3 | interface ChatRoom : Entity { 4 | 5 | val name: String? 6 | val ownerId: String? 7 | val iconUrl: String? 8 | 9 | suspend fun isMember(userId: String): Boolean 10 | 11 | suspend fun isMember(user: User): Boolean = 12 | isMember(user.id) 13 | } 14 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/entity/CustomEmoji.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.entity 2 | 3 | interface CustomEmoji : Mentionable { 4 | 5 | val name: String 6 | val imageUrl: String 7 | } 8 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/entity/DisplayedUser.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.entity 2 | 3 | interface DisplayedUser : Mentionable { 4 | 5 | val effectiveName: String 6 | val effectiveAvatarUrl: String 7 | } 8 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/entity/Entity.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.entity 2 | 3 | interface Entity : Managed { 4 | 5 | val id: String 6 | } 7 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/entity/FakeMessage.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.entity 2 | 3 | import io.github.shaksternano.borgar.messaging.BotManager 4 | import io.github.shaksternano.borgar.messaging.builder.MessageEditBuilder 5 | import io.github.shaksternano.borgar.messaging.entity.channel.Channel 6 | import io.github.shaksternano.borgar.messaging.entity.channel.MessageChannel 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.emptyFlow 9 | import java.time.OffsetDateTime 10 | 11 | data class FakeMessage( 12 | override val id: String, 13 | override val content: String, 14 | private val author: User, 15 | private val channel: MessageChannel, 16 | override val timeCreated: OffsetDateTime = OffsetDateTime.now(), 17 | ) : Message { 18 | 19 | override val manager: BotManager = author.manager 20 | override val authorId: String = author.id 21 | override val referencedMessages: Flow = emptyFlow() 22 | 23 | override val mentionedUsers: Flow = manager.getMentionedUsers(content) 24 | override val mentionedChannels: Flow = manager.getMentionedChannels(content) 25 | override val mentionedRoles: Flow = manager.getMentionedRoles(content) 26 | 27 | override val attachments: List = listOf() 28 | override val customEmojis: Flow = manager.getCustomEmojis(content) 29 | override val stickers: Flow = emptyFlow() 30 | 31 | override val link: String = "" 32 | 33 | override suspend fun getAuthor(): User = author 34 | 35 | override suspend fun getAuthorMember(): Member? = getGuild()?.getMember(author) 36 | 37 | override suspend fun getChannel(): MessageChannel = channel 38 | 39 | override suspend fun getGuild(): Guild? = channel.getGuild() 40 | 41 | override suspend fun getGroup(): Group? = channel.getGroup() 42 | 43 | override suspend fun getEmbeds(): List = emptyList() 44 | 45 | override suspend fun edit(block: MessageEditBuilder.() -> Unit): Message { 46 | val builder = MessageEditBuilder().apply(block) 47 | return builder.content?.let { 48 | copy( 49 | content = it, 50 | timeCreated = OffsetDateTime.now() 51 | ) 52 | } ?: this 53 | } 54 | 55 | override suspend fun delete() = Unit 56 | } 57 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/entity/Group.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.entity 2 | 3 | interface Group : ChatRoom 4 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/entity/Guild.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.entity 2 | 3 | import io.github.shaksternano.borgar.messaging.command.Command 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface Guild : ChatRoom { 7 | 8 | val bannerUrl: String? 9 | val splashUrl: String? 10 | val maxFileSize: Long 11 | val publicRole: Role? 12 | 13 | suspend fun getMember(userId: String): Member? 14 | 15 | suspend fun getMember(user: User): Member? = getMember(user.id) 16 | 17 | override suspend fun isMember(userId: String): Boolean = 18 | getMember(userId) != null 19 | 20 | fun getEmojis(): Flow 21 | 22 | suspend fun addCommand(command: Command) 23 | 24 | suspend fun deleteCommand(commandName: String) 25 | } 26 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/entity/Managed.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.entity 2 | 3 | import io.github.shaksternano.borgar.messaging.BotManager 4 | 5 | interface Managed { 6 | 7 | val manager: BotManager 8 | } -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/entity/Member.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.entity 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import java.time.OffsetDateTime 5 | 6 | interface Member : DisplayedUser, PermissionHolder { 7 | 8 | val user: User 9 | val roles: Flow 10 | val timeoutEnd: OffsetDateTime? 11 | 12 | suspend fun getGuild(): Guild 13 | 14 | suspend fun isOwner(): Boolean = 15 | id == getGuild().ownerId 16 | } 17 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/entity/Mentionable.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.entity 2 | 3 | interface Mentionable : Entity { 4 | 5 | val asMention: String 6 | val asBasicMention: String 7 | } 8 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/entity/Message.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.entity 2 | 3 | import io.github.shaksternano.borgar.messaging.builder.MessageCreateBuilder 4 | import io.github.shaksternano.borgar.messaging.builder.MessageEditBuilder 5 | import io.github.shaksternano.borgar.messaging.command.CommandMessageIntersection 6 | 7 | interface Message : CommandMessageIntersection, TimeStamped { 8 | 9 | val link: String 10 | 11 | suspend fun reply(content: String): Message = reply { 12 | this.content = content 13 | } 14 | 15 | suspend fun reply(block: MessageCreateBuilder.() -> Unit): Message = getChannel()?.createMessage { 16 | block() 17 | referencedMessageIds.clear() 18 | referencedMessageIds.add(id) 19 | } ?: error("Message channel not found") 20 | 21 | suspend fun edit(content: String): Message = edit { 22 | this.content = content 23 | } 24 | 25 | suspend fun edit(block: MessageEditBuilder.() -> Unit): Message 26 | 27 | suspend fun delete() 28 | } 29 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/entity/MessageEmbed.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.entity 2 | 3 | import io.github.shaksternano.borgar.core.io.UrlInfo 4 | import io.github.shaksternano.borgar.core.util.TenorMediaType 5 | import io.github.shaksternano.borgar.core.util.isTenorUrl 6 | import io.github.shaksternano.borgar.core.util.retrieveTenorMediaUrl 7 | 8 | data class MessageEmbed( 9 | val url: String?, 10 | val image: ImageInfo? = null, 11 | val video: VideoInfo? = null, 12 | val thumbnail: ThumbnailInfo? = null, 13 | ) { 14 | 15 | data class ImageInfo( 16 | val url: String?, 17 | val proxyUrl: String?, 18 | ) 19 | 20 | data class VideoInfo( 21 | val url: String?, 22 | val proxyUrl: String?, 23 | ) 24 | 25 | data class ThumbnailInfo( 26 | val url: String?, 27 | val proxyUrl: String?, 28 | ) 29 | } 30 | 31 | suspend fun MessageEmbed.getContent(getGif: Boolean = false): UrlInfo? { 32 | val isTenor = url?.let(::isTenorUrl) == true 33 | val tenorGifUrl = if (getGif && url != null) 34 | retrieveTenorMediaUrl(url, TenorMediaType.GIF_LARGE) 35 | else null 36 | val url = tenorGifUrl 37 | ?: video?.proxyUrl 38 | ?: video?.url 39 | ?: image?.proxyUrl 40 | ?: image?.url 41 | ?: thumbnail?.proxyUrl 42 | ?: thumbnail?.url 43 | return url?.let { 44 | UrlInfo( 45 | url = it, 46 | gifv = isTenor && tenorGifUrl == null 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/entity/PermissionHolder.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.entity 2 | 3 | import io.github.shaksternano.borgar.messaging.command.Permission 4 | import io.github.shaksternano.borgar.messaging.entity.channel.Channel 5 | 6 | interface PermissionHolder : Entity { 7 | 8 | suspend fun hasPermission(permissions: Set): Boolean 9 | 10 | suspend fun hasPermission(permissions: Set, channel: Channel): Boolean 11 | } 12 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/entity/Role.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.entity 2 | 3 | interface Role : Mentionable, PermissionHolder { 4 | 5 | val name: String 6 | } 7 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/entity/Sticker.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.entity 2 | 3 | interface Sticker : Entity { 4 | 5 | val name: String 6 | val imageUrl: String 7 | } 8 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/entity/TimeStamped.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.entity 2 | 3 | import java.time.OffsetDateTime 4 | 5 | interface TimeStamped { 6 | val timeCreated: OffsetDateTime 7 | } 8 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/entity/User.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.entity 2 | 3 | interface User : DisplayedUser { 4 | 5 | val name: String 6 | val isSelf: Boolean 7 | val isBot: Boolean 8 | 9 | suspend fun getBannerUrl(): String? 10 | } 11 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/entity/channel/Channel.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.entity.channel 2 | 3 | import io.github.shaksternano.borgar.core.util.ChannelEnvironment 4 | import io.github.shaksternano.borgar.messaging.entity.Group 5 | import io.github.shaksternano.borgar.messaging.entity.Guild 6 | import io.github.shaksternano.borgar.messaging.entity.Mentionable 7 | 8 | interface Channel : Mentionable { 9 | 10 | val name: String 11 | val environment: ChannelEnvironment 12 | 13 | suspend fun getGuild(): Guild? 14 | 15 | suspend fun getGroup(): Group? 16 | 17 | suspend fun getMaxFileSize(): Long { 18 | return getGuild()?.maxFileSize ?: manager.maxFileSize 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/entity/channel/MessageChannel.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.entity.channel 2 | 3 | import io.github.shaksternano.borgar.messaging.builder.MessageCreateBuilder 4 | import io.github.shaksternano.borgar.messaging.entity.Message 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | interface MessageChannel : Channel { 8 | 9 | val cancellableTyping: Boolean 10 | 11 | suspend fun sendTyping() 12 | 13 | suspend fun stopTyping() 14 | 15 | suspend fun createMessage(content: String): Message = createMessage { 16 | this.content = content 17 | } 18 | 19 | suspend fun createMessage(block: MessageCreateBuilder.() -> Unit): Message 20 | 21 | fun getPreviousMessages(beforeId: String): Flow 22 | } 23 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/event/CommandEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.event 2 | 3 | import io.github.shaksternano.borgar.core.util.ChannelEnvironment 4 | import io.github.shaksternano.borgar.messaging.command.CommandArguments 5 | import io.github.shaksternano.borgar.messaging.command.CommandMessageIntersection 6 | import io.github.shaksternano.borgar.messaging.command.CommandResponse 7 | import io.github.shaksternano.borgar.messaging.entity.* 8 | import io.github.shaksternano.borgar.messaging.entity.channel.MessageChannel 9 | import kotlinx.coroutines.flow.Flow 10 | 11 | interface CommandEvent : Managed, TimeStamped { 12 | 13 | val id: String 14 | val authorId: String 15 | val referencedMessages: Flow 16 | var ephemeralReply: Boolean 17 | 18 | suspend fun getAuthor(): User 19 | 20 | suspend fun getAuthorMember(): Member? 21 | 22 | suspend fun getChannel(): MessageChannel 23 | 24 | suspend fun getEnvironment(): ChannelEnvironment 25 | 26 | suspend fun getGuild(): Guild? 27 | 28 | suspend fun getGroup(): Group? 29 | 30 | suspend fun deferReply() 31 | 32 | suspend fun reply(response: CommandResponse): Message 33 | 34 | suspend fun reply(content: String): Message = reply(CommandResponse(content)) 35 | 36 | fun asMessageIntersection(arguments: CommandArguments): CommandMessageIntersection 37 | } 38 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/event/Event.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.event 2 | 3 | import io.github.shaksternano.borgar.messaging.entity.Managed 4 | 5 | interface Event : Managed 6 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/event/MessageCommandEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.event 2 | 3 | import io.github.shaksternano.borgar.core.util.ChannelEnvironment 4 | import io.github.shaksternano.borgar.messaging.BotManager 5 | import io.github.shaksternano.borgar.messaging.command.CommandArguments 6 | import io.github.shaksternano.borgar.messaging.command.CommandMessageIntersection 7 | import io.github.shaksternano.borgar.messaging.command.CommandResponse 8 | import io.github.shaksternano.borgar.messaging.entity.* 9 | import io.github.shaksternano.borgar.messaging.entity.channel.MessageChannel 10 | import kotlinx.coroutines.flow.Flow 11 | import java.time.OffsetDateTime 12 | 13 | data class MessageCommandEvent( 14 | private val event: MessageReceiveEvent, 15 | ) : CommandEvent { 16 | 17 | override val manager: BotManager = event.manager 18 | override val id: String = event.messageId 19 | override val authorId: String = event.authorId 20 | override val timeCreated: OffsetDateTime = event.message.timeCreated 21 | override val referencedMessages: Flow = event.message.referencedMessages 22 | override var ephemeralReply: Boolean = false 23 | private var replied: Boolean = false 24 | 25 | override suspend fun getAuthor(): User = event.getAuthor() 26 | 27 | override suspend fun getAuthorMember(): Member? = event.getAuthorMember() 28 | 29 | override suspend fun getChannel(): MessageChannel = event.getChannel() 30 | 31 | override suspend fun getEnvironment(): ChannelEnvironment = event.getEnvironment() 32 | 33 | override suspend fun getGuild(): Guild? = event.getGuild() 34 | 35 | override suspend fun getGroup(): Group? = event.getGroup() 36 | 37 | override suspend fun deferReply() = Unit 38 | 39 | override suspend fun reply(response: CommandResponse): Message = event.getChannel().createMessage { 40 | fromCommandResponse(response) 41 | if (!replied) { 42 | replied = true 43 | referencedMessageIds.add(event.messageId) 44 | } 45 | } 46 | 47 | override fun asMessageIntersection(arguments: CommandArguments): CommandMessageIntersection = 48 | event.message 49 | } 50 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/event/MessageReceiveEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.event 2 | 3 | import io.github.shaksternano.borgar.messaging.BotManager 4 | import io.github.shaksternano.borgar.messaging.builder.MessageCreateBuilder 5 | import io.github.shaksternano.borgar.messaging.entity.Guild 6 | import io.github.shaksternano.borgar.messaging.entity.Member 7 | import io.github.shaksternano.borgar.messaging.entity.Message 8 | import io.github.shaksternano.borgar.messaging.entity.User 9 | import io.github.shaksternano.borgar.messaging.entity.channel.MessageChannel 10 | 11 | class MessageReceiveEvent( 12 | val message: Message, 13 | ) : Event { 14 | 15 | override val manager: BotManager = message.manager 16 | val messageId = message.id 17 | val authorId = message.authorId 18 | 19 | suspend fun getAuthor(): User = message.getAuthor() 20 | 21 | suspend fun getAuthorMember(): Member? = message.getAuthorMember() 22 | 23 | suspend fun getChannel(): MessageChannel = message.getChannel() 24 | ?: error("Message channel not found") 25 | 26 | suspend fun getEnvironment() = getChannel().environment 27 | 28 | suspend fun getGuild(): Guild? = message.getGuild() 29 | 30 | suspend fun getGroup() = message.getGroup() 31 | 32 | suspend fun reply(content: String): Message = message.reply(content) 33 | 34 | suspend fun reply(block: MessageCreateBuilder.() -> Unit): Message = message.reply(block) 35 | } 36 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/exception/Exceptions.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.exception 2 | 3 | import io.github.shaksternano.borgar.messaging.command.CommandConfig 4 | 5 | class CommandException( 6 | val commandConfigs: List, 7 | override val message: String = "", 8 | override val cause: Throwable? = null, 9 | ) : Exception(message, cause) 10 | 11 | class MissingArgumentException( 12 | override val message: String, 13 | ) : Exception(message) 14 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/util/FavouriteHandler.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.util 2 | 3 | import io.github.shaksternano.borgar.core.io.filenameWithoutExtension 4 | import io.github.shaksternano.borgar.core.io.removeQueryParams 5 | import io.github.shaksternano.borgar.core.util.getUrls 6 | import io.github.shaksternano.borgar.messaging.command.FAVOURITE_ALIAS_PREFIX 7 | import io.github.shaksternano.borgar.messaging.event.MessageReceiveEvent 8 | import kotlinx.coroutines.coroutineScope 9 | import kotlinx.coroutines.launch 10 | import java.util.* 11 | 12 | suspend fun sendFavouriteFile(event: MessageReceiveEvent) { 13 | val message = event.message 14 | val urls = message.content.getUrls().ifEmpty { return } 15 | val aliasUrl = removeQueryParams(urls.first()) 16 | val fileName = filenameWithoutExtension(aliasUrl) 17 | if (!fileName.startsWith(FAVOURITE_ALIAS_PREFIX)) return 18 | val url = getUrl(aliasUrl) ?: return 19 | val author = event.getAuthorMember() ?: event.getAuthor() 20 | val channel = event.getChannel() 21 | coroutineScope { 22 | val guild = event.getGuild() 23 | launch { 24 | if (guild == null) { 25 | channel.createMessage(url) 26 | } else { 27 | runCatching { 28 | channel.createMessage { 29 | content = url 30 | username = author.effectiveName 31 | avatarUrl = author.effectiveAvatarUrl 32 | } 33 | }.onFailure { 34 | channel.createMessage(url) 35 | } 36 | } 37 | } 38 | launch { 39 | if (guild == null) return@launch 40 | runCatching { 41 | event.message.delete() 42 | } 43 | } 44 | } 45 | } 46 | 47 | private fun getUrl(aliasUrl: String): String? { 48 | val filename = filenameWithoutExtension(aliasUrl) 49 | val nameParts = filename.split("_", limit = 2) 50 | if (nameParts.size != 2) return null 51 | val decodedBytes = runCatching { 52 | Base64.getDecoder().decode(nameParts[1]) 53 | }.getOrElse { return null } 54 | return String(decodedBytes) 55 | } 56 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/util/MessageListener.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.util 2 | 3 | import io.github.shaksternano.borgar.core.logger 4 | import io.github.shaksternano.borgar.messaging.command.parseAndExecuteCommand 5 | import io.github.shaksternano.borgar.messaging.event.MessageReceiveEvent 6 | 7 | suspend fun onMessageReceived(event: MessageReceiveEvent) { 8 | runCatching { 9 | if (event.authorId == event.manager.selfId) return 10 | parseAndExecuteCommand(event) 11 | sendFavouriteFile(event) 12 | }.onFailure { 13 | logger.error("Error handling message event", it) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /messaging/src/main/kotlin/io/github/shaksternano/borgar/messaging/util/SelectedMessages.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.messaging.util 2 | 3 | import com.google.common.cache.Cache 4 | import com.google.common.cache.CacheBuilder 5 | import io.github.shaksternano.borgar.core.collect.getAndInvalidate 6 | import io.github.shaksternano.borgar.messaging.MessagingPlatform 7 | import io.github.shaksternano.borgar.messaging.entity.Message 8 | import java.time.Duration 9 | 10 | private data class SelectedMessageKey( 11 | val userId: String, 12 | val channelId: String, 13 | val platform: MessagingPlatform, 14 | ) 15 | 16 | private val selectedMessages: Cache = CacheBuilder.newBuilder() 17 | .expireAfterWrite(Duration.ofHours(1)) 18 | .build() 19 | private val lowPrioritySelectedMessages: Cache = CacheBuilder.newBuilder() 20 | .expireAfterWrite(Duration.ofHours(1)) 21 | .build() 22 | 23 | fun setSelectedMessage(userId: String, channelId: String, platform: MessagingPlatform, message: Message) { 24 | selectedMessages.put( 25 | SelectedMessageKey( 26 | userId = userId, 27 | channelId = channelId, 28 | platform = platform, 29 | ), 30 | message, 31 | ) 32 | } 33 | 34 | fun getAndExpireSelectedMessage( 35 | userId: String, 36 | channelId: String, 37 | platform: MessagingPlatform, 38 | ): Message? { 39 | val key = SelectedMessageKey(userId, channelId, platform) 40 | return selectedMessages.getAndInvalidate(key) 41 | } 42 | 43 | fun setLowPrioritySelectedMessage(userId: String, channelId: String, platform: MessagingPlatform, message: Message) { 44 | lowPrioritySelectedMessages.put( 45 | SelectedMessageKey( 46 | userId = userId, 47 | channelId = channelId, 48 | platform = platform, 49 | ), 50 | message, 51 | ) 52 | } 53 | 54 | fun getAndExpireLowPrioritySelectedMessage( 55 | userId: String, 56 | channelId: String, 57 | platform: MessagingPlatform, 58 | ): Message? { 59 | val key = SelectedMessageKey(userId, channelId, platform) 60 | return lowPrioritySelectedMessages.getAndInvalidate(key) 61 | } 62 | -------------------------------------------------------------------------------- /revolt/build.gradle.kts: -------------------------------------------------------------------------------- 1 | val ulidKotlinVersion: String by project 2 | 3 | dependencies { 4 | api(project(":messaging")) 5 | 6 | implementation("com.aallam.ulid:ulid-kotlin:$ulidKotlinVersion") 7 | 8 | testImplementation(kotlin("test")) 9 | } 10 | -------------------------------------------------------------------------------- /revolt/src/main/kotlin/io/github/shaksternano/borgar/revolt/Revolt.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.revolt 2 | 3 | import io.github.shaksternano.borgar.messaging.registerBotManager 4 | 5 | suspend fun initRevolt(token: String) { 6 | val manager = RevoltManager(token) 7 | manager.init() 8 | registerBotManager(manager) 9 | } 10 | -------------------------------------------------------------------------------- /revolt/src/main/kotlin/io/github/shaksternano/borgar/revolt/entity/RevoltCustomEmoji.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.revolt.entity 2 | 3 | import io.github.shaksternano.borgar.messaging.entity.BaseEntity 4 | import io.github.shaksternano.borgar.messaging.entity.CustomEmoji 5 | import io.github.shaksternano.borgar.revolt.RevoltManager 6 | import kotlinx.serialization.SerialName 7 | import kotlinx.serialization.Serializable 8 | 9 | data class RevoltCustomEmoji( 10 | override val id: String, 11 | override val name: String, 12 | override val imageUrl: String, 13 | override val manager: RevoltManager, 14 | ) : CustomEmoji, BaseEntity() { 15 | 16 | override val asMention: String = manager.emojiAsTyped(id) 17 | override val asBasicMention: String = manager.emojiAsTyped(id) 18 | } 19 | 20 | @Serializable 21 | data class RevoltEmojiResponse( 22 | @SerialName("_id") 23 | val id: String, 24 | val name: String, 25 | val animated: Boolean = false, 26 | ) { 27 | fun convert(manager: RevoltManager): RevoltCustomEmoji { 28 | return RevoltCustomEmoji( 29 | id = id, 30 | name = name, 31 | imageUrl = "${manager.cdnUrl}/emojis/$id/original", 32 | manager = manager, 33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /revolt/src/main/kotlin/io/github/shaksternano/borgar/revolt/entity/RevoltGroup.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.revolt.entity 2 | 3 | import io.github.shaksternano.borgar.messaging.entity.BaseEntity 4 | import io.github.shaksternano.borgar.messaging.entity.Group 5 | import io.github.shaksternano.borgar.revolt.RevoltManager 6 | 7 | data class RevoltGroup( 8 | override val manager: RevoltManager, 9 | override val id: String, 10 | override val name: String?, 11 | override val ownerId: String?, 12 | private val memberIds: Set, 13 | override val iconUrl: String?, 14 | ) : Group, BaseEntity() { 15 | 16 | override suspend fun isMember(userId: String): Boolean = 17 | memberIds.contains(userId) 18 | } 19 | -------------------------------------------------------------------------------- /revolt/src/main/kotlin/io/github/shaksternano/borgar/revolt/entity/channel/RevoltChannelType.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.revolt.entity.channel 2 | 3 | enum class RevoltChannelType( 4 | val apiName: String, 5 | ) { 6 | SAVED_MESSAGES("SavedMessages"), 7 | DIRECT_MESSAGE("DirectMessage"), 8 | GROUP("Group"), 9 | TEXT("TextChannel"), 10 | VOICE("VoiceChannel"), 11 | UNKNOWN("Unknown"), 12 | ; 13 | 14 | companion object { 15 | fun fromApiName(apiName: String): RevoltChannelType = 16 | entries.find { it.apiName == apiName } ?: UNKNOWN 17 | } 18 | 19 | fun isMessage(): Boolean = 20 | this == DIRECT_MESSAGE 21 | || this == GROUP 22 | || this == TEXT 23 | } 24 | -------------------------------------------------------------------------------- /revolt/src/main/kotlin/io/github/shaksternano/borgar/revolt/websocket/WebSocketMessageHandler.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.revolt.websocket 2 | 3 | import kotlinx.serialization.json.JsonObject 4 | 5 | fun interface WebSocketMessageHandler { 6 | 7 | suspend fun handleMessage(json: JsonObject) 8 | } 9 | -------------------------------------------------------------------------------- /revolt/src/main/kotlin/io/github/shaksternano/borgar/revolt/websocket/WebSocketMessageType.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.revolt.websocket 2 | 3 | enum class WebSocketMessageType( 4 | val apiName: String, 5 | ) { 6 | READY("Ready"), 7 | AUTHENTICATED("Authenticated"), 8 | NOT_FOUND("NotFound"), 9 | MESSAGE("Message"), 10 | SERVER_CREATE("ServerCreate"), 11 | SERVER_DELETE("ServerDelete"), 12 | SERVER_MEMBER_LEAVE("ServerMemberLeave"), 13 | CHANNEL_CREATE("ChannelCreate"), 14 | CHANNEL_GROUP_LEAVE("ChannelGroupLeave"), 15 | } 16 | -------------------------------------------------------------------------------- /scripts/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api(project(":core")) 3 | } 4 | 5 | tasks { 6 | jar { 7 | enabled = false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /scripts/src/main/kotlin/io/github/shaksternano/borgar/scripts/emoji/EmojiShortcodesFileGenerator.kt: -------------------------------------------------------------------------------- 1 | package io.github.shaksternano.borgar.scripts.emoji 2 | 3 | import io.github.shaksternano.borgar.core.emoji.EMOJI_FILES_DIRECTORY 4 | import io.github.shaksternano.borgar.core.io.get 5 | import io.github.shaksternano.borgar.core.io.useHttpClient 6 | import io.github.shaksternano.borgar.core.util.prettyPrintJson 7 | import io.ktor.client.statement.* 8 | import org.slf4j.Logger 9 | import org.slf4j.LoggerFactory 10 | import kotlin.io.path.* 11 | import kotlin.time.DurationUnit 12 | import kotlin.time.TimeSource 13 | 14 | private val emojiLogger: Logger = LoggerFactory.getLogger("Emoji Shortcodes File Generator") 15 | private const val EMOJI_SHORTCODES_FILE_NAME: String = "emojis.json" 16 | 17 | suspend fun main() { 18 | val startTime = TimeSource.Monotonic.markNow() 19 | emojiLogger.info("Starting!") 20 | val directory = Path(EMOJI_FILES_DIRECTORY) 21 | directory.createDirectories() 22 | if (directory.isDirectory()) { 23 | val emojiShortcodesFile = directory.resolve(EMOJI_SHORTCODES_FILE_NAME) 24 | runCatching { 25 | emojiShortcodesFile.createFile() 26 | } 27 | runCatching { 28 | val emojiJsonString = useHttpClient { client -> 29 | val response = 30 | client.get("https://raw.githubusercontent.com/ArkinSolomon/discord-emoji-converter/master/emojis.json") 31 | response.bodyAsText() 32 | } 33 | val prettyPrintedJson = prettyPrintJson(emojiJsonString) 34 | emojiShortcodesFile.writeText(prettyPrintedJson + "\n") 35 | val time = TimeSource.Monotonic.markNow() 36 | val timeTaken = time - startTime 37 | val timeTakenString = timeTaken.toString(DurationUnit.SECONDS, 3) 38 | emojiLogger.info("Created emoji shortcodes file $emojiShortcodesFile in $timeTakenString") 39 | }.onFailure { 40 | emojiLogger.error("Error downloading emoji shortcodes file", it) 41 | } 42 | } else if (directory.isRegularFile()) { 43 | emojiLogger.error("Failed to create emoji shortcodes file! The directory path $directory already exists as a file") 44 | } else { 45 | emojiLogger.error("Failed to create emoji shortcodes file! Could not create parent directory $directory") 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | } 5 | 6 | fun PluginResolveDetails.applyPluginVersion(property: String) { 7 | gradle.rootProject.extra[property]?.let { 8 | useVersion(it as String) 9 | } 10 | } 11 | 12 | resolutionStrategy { 13 | eachPlugin { 14 | val pluginId = requested.id.id 15 | if (pluginId == "org.jetbrains.kotlinx.atomicfu") { 16 | applyPluginVersion("kotlinxAtomicFuVersion") 17 | } else if (pluginId.startsWith("org.jetbrains.kotlin")) { 18 | applyPluginVersion("kotlinVersion") 19 | } 20 | } 21 | } 22 | } 23 | 24 | plugins { 25 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" 26 | } 27 | 28 | rootProject.name = "Borgar" 29 | 30 | include( 31 | "app", 32 | "core", 33 | "discord", 34 | "messaging", 35 | "revolt", 36 | "scripts", 37 | ) 38 | --------------------------------------------------------------------------------