├── .dockerignore ├── .github └── FUNDING.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── build.gradle.kts ├── config.example.yml ├── docker-compose.yml ├── extras ├── bot_perms.png └── research │ └── UI Dev │ ├── IMG_20220903_140054.jpg │ └── IMG_20220903_140521.jpg ├── flow_server.yml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts ├── src └── main │ ├── kotlin │ ├── Config.kt │ ├── Consts.kt │ ├── Main.kt │ ├── client │ │ ├── QueueClient.kt │ │ ├── QueueEntry.kt │ │ ├── Test.kt │ │ └── blake3.kt │ ├── commands │ │ ├── art_contest │ │ │ ├── Leaderboard.kt │ │ │ ├── RemoveFromContestCommand.kt │ │ │ ├── SubmitToContestCommand.kt │ │ │ ├── TestLeaderboard.kt │ │ │ ├── Vote.kt │ │ │ └── cancel │ │ │ │ └── CancelCommand.kt │ │ ├── chapters │ │ │ ├── ListChaptersCommand.kt │ │ │ └── RollbackCommand.kt │ │ ├── edit │ │ │ ├── EditImageCommand.kt │ │ │ ├── RemoveBackgroundImageCommand.kt │ │ │ └── UpscaleImageCommand.kt │ │ ├── magic_prompt │ │ │ └── MagicPromptCommand.kt │ │ ├── make │ │ │ ├── MakeAudioCommand.kt │ │ │ ├── MakeCommand.kt │ │ │ ├── MakeImageCommand.kt │ │ │ ├── QuiltMaker.kt │ │ │ ├── Ratio.kt │ │ │ └── Utils.kt │ │ ├── social │ │ │ ├── EditProfile.kt │ │ │ ├── Favorites.kt │ │ │ ├── Profile.kt │ │ │ ├── ServerStats.kt │ │ │ └── Share.kt │ │ ├── train │ │ │ ├── DownloadTrainingCommand.kt │ │ │ └── TrainCommand.kt │ │ ├── update │ │ │ └── UpdateCommand.kt │ │ └── variate │ │ │ └── VariateCommand.kt │ ├── database │ │ ├── Database.kt │ │ └── models │ │ │ ├── ArtContestEntry.kt │ │ │ ├── ArtContestVote.kt │ │ │ ├── ChapterEntry.kt │ │ │ ├── SharedArtCacheEntry.kt │ │ │ ├── Thingy.kt │ │ │ ├── User.kt │ │ │ └── UserChapter.kt │ ├── ui │ │ ├── ImageUploadUI.kt │ │ ├── PaginationUI.kt │ │ └── RealPaginator.kt │ └── utils │ │ ├── BiMap.kt │ │ ├── BufferedImageToByteArray.kt │ │ ├── ByteArrayImageTo.kt │ │ ├── CaseUtils.kt │ │ ├── CheckForEmbeds.kt │ │ ├── CommandEventToJson.kt │ │ ├── DocExt.kt │ │ ├── DocUtils.kt │ │ ├── DoubleExt.kt │ │ ├── EtaUtils.kt │ │ ├── GetAverageColor.kt │ │ ├── GetResourceAsText.kt │ │ ├── ImageConversionUtils.kt │ │ ├── ImageFromURL.kt │ │ ├── JsonUtils.kt │ │ ├── MakeProfileCard.kt │ │ ├── MessageToURL.kt │ │ ├── MessageUtils.kt │ │ ├── PeterDate.kt │ │ ├── ProgressBar.kt │ │ ├── Random.kt │ │ ├── ResizeImage.kt │ │ ├── Sanitize.kt │ │ ├── StringUtils.kt │ │ ├── StripHiddenParameters.kt │ │ ├── TakeSlice.kt │ │ ├── ToThumbnail.kt │ │ └── ValidatePermissions.kt │ ├── proto │ ├── docarray.proto │ └── jina.proto │ └── resources │ ├── logback.xml │ └── scripts │ ├── bark.py │ ├── deep_floyd_if_s1.py │ ├── deep_floyd_if_s2.py │ ├── deep_floyd_if_s3.py │ ├── deep_floyd_if_s4.py │ ├── deliberate.py │ ├── edit_image.py │ ├── kadinsky_gen.py │ ├── kadinsky_prior.py │ ├── musicgen.py │ ├── photon.py │ ├── remove_bg_image.py │ ├── sd_xl.py │ ├── stable_diffusion_512.py │ ├── stable_diffusion_768.py │ ├── stable_diffusion_upscale.py │ ├── test.py │ └── ti_booster.py └── thingy3 ├── .gitignore ├── Cargo.toml └── src ├── discord_bot ├── handler.rs └── mod.rs ├── main.rs └── vm ├── command.rs ├── mod.rs ├── modules ├── mod.rs └── serenity │ ├── builder │ ├── create_application_command.rs │ └── mod.rs │ └── mod.rs └── types.rs /.dockerignore: -------------------------------------------------------------------------------- 1 | /thingy3 2 | .idea/ 3 | .gradle/ 4 | build/ 5 | config.yml 6 | *.sqlite 7 | Dockerfile 8 | docker-compose.yml 9 | config.example.yml 10 | README.md 11 | .gitignore 12 | LICENSE -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: emerald_show # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .gradle/ 3 | build/ 4 | config.yml 5 | *.sqlite 6 | /queue_entry_executor/text-inversion-model -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gradle:jdk18-jammy as thingy_builder 2 | WORKDIR /usr/src/app 3 | COPY . . 4 | RUN gradle :shadowJar 5 | 6 | FROM debian:bullseye 7 | RUN echo "Installing system dependencies" && \ 8 | apt-get update && \ 9 | apt-get install -y software-properties-common python3-pip wget curl && \ 10 | wget -O- https://apt.corretto.aws/corretto.key | apt-key add - && \ 11 | add-apt-repository 'deb https://apt.corretto.aws stable main' && \ 12 | apt-get update && \ 13 | apt-get install -y java-18-amazon-corretto-jdk && \ 14 | apt-get remove -y wget software-properties-common && \ 15 | apt-get autoremove -y && \ 16 | rm -rf corretto.key && \ 17 | rm -rf /var/lib/apt/lists/* 18 | 19 | WORKDIR /opt/thingy/data 20 | COPY --from=thingy_builder /usr/src/app/build/libs/Thingy-*-all.jar /opt/thingy/Thingy.jar 21 | CMD ["java", "-jar", "/opt/thingy/Thingy.jar"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Peter Willemsen 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Thingy 2 | 3 | Discord bot to generate images based on a text prompt - way more than just that! Through a wide variety of tools, you can alter your generated images and share them with friends. 4 | 5 | Integrates with DiscoArt, Stable Diffusion and other peculiarities, all rolled up in a single, sexy user interface that allows one to mix and match different configurations without a steeper learning curve! 6 | 7 | - Powerful customization tools 8 | - Fairness queue: Making sure everyone gets to make art! 9 | - Social features: Sharing, profiles and stats 10 | - Advanced Profile customization 11 | - A massive upscaling feature with the ability to upscale up to 3k 12 | - **Chapters**: Work on your pieces, and switch back to previous works of art! 13 | - Every "creation" command (`/make_image`, `/make_audio` results in a new chapter 14 | - Use variation commands (`/upscale`, `/variate`, ~~more to come~~) to alter said chapters as they wish 15 | - Don't like a change? Just do `/rollback`! There's unlimited undo! 16 | - Instantly swap back to previous chapters with `/chapters` 17 | 18 | [Demo video + dev journey](https://www.youtube.com/watch?v=epLF0OXTp-A) 19 | 20 | In short: this bot allows you to generate images based on a text prompt, but can do way more than just that - offering a wide variety of tools to make alter your generated images. 21 | 22 | # Demo art 23 | 24 | ![black smoke escaping out of a human mouth, diesel exhaust realistic portrait](https://cdn.discordapp.com/attachments/1015964690800726158/1047917600161681508/final.jpg) 25 | 26 | ![Hamburger with jalapeños served on a wooden log slice, centered, food, snack, realistic photo, kodak Ektar](https://cdn.discordapp.com/attachments/1015964690800726158/1047429960056447036/final.png) 27 | 28 | # Credits and special thanks 29 | 30 | - First and foremost Han Xiao for being around in DMs helping me with what I struggled with but also putting me on the right direction in various moments 31 | - [DiscoArt](https://github.com/jina-ai/discoart): without this project, I was never able to cook this up in a weekend 32 | - [Jina](https://jina.ai) which has some incredible tooling I got familiar with 33 | - [Mahdi Chaker](https://twitter.com/MahdiMC) for the heavy training GPUs for LEAP + Running the bot on my Discord server! 34 | 35 | # Demo 36 | 37 | [My discord server](https://discord.gg/j4wQYhhvVd) has the bot running, both alpha and production bots are up! 38 | 39 | # Run it yourself! 40 | 41 | As this bot is open-source, anyone can run it. Depending on the method, you need different specs. The easiest is through Docker Compose as it will instantly set up everything you need. 42 | 43 | The bot consists of 2 major parts: 44 | 45 | - Thingy (the interface where users will interact with) 46 | - Hydrolane (the queue system and AI loader) (which will actually run the models) 47 | 48 | In the tutorial below, we will set up both components so you can get started. 49 | 50 | ## Installing via Docker Compose 51 | 52 | **Note!** As we evolved from a weekend project to a larger-scale bot, things have changed a lot, and I thank you all. Please, if you use this in your own server, do get in touch with me, so we can see what works and what doesn't. Any feedback is really appreciated! Feel free to join [Thingy's birthplace](https://discord.gg/j4wQYhhvVd) or shoot a line in the projects Issues page! 53 | 54 | **What you need**: 55 | 56 | - Linux or Windows with Docker installed (Or [download here](https://docs.docker.com/get-docker/)) 57 | - Discord account and bot (more on this later) 58 | - A Nvidia GPU (Todo: support AMD, CPU, etc. Let me know if you can help testing!) 59 | 60 | **Steps**: 61 | 62 | - Create a directory where we will set up our bot, and inside this directory, create a directory called `thingy_data` 63 | - If you ran an older version of the bot, you can drop your `db.sqlite` here. Keep in mind the configuration has changed slightly. Otherwise, you can ignore this. 64 | - Download [config.example.yml](config.example.yml) and name it `config.yml`, put it into the bot directory. 65 | - Once done, follow "Setting up the bot" and then go back here. 66 | - Download [docker-compose.yml](docker-compose.yml), if you use a Nvidia GPU then you don't need to change anything. Drop this file in the same directory as where your data directory resides. 67 | - You should now have the following list of files (correct your setup to match this tree if necessary): 68 | ``` 69 | ├── docker-compose.yml 70 | └── thingy_data 71 | └── config.yml 72 | ``` 73 | - You're ready! Run `docker compose up -d` to start the network. Docker automatically manages downtime and user data is stored in the `thingy_data` directory! 74 | - Now you should be able to run `/make_image` and other peculiarities in Discord! The first time it might take a while to run due to downloading models. 75 | 76 | ## Setting up the bot 77 | 78 | - Create a Discord application on their [developer portal](https://discord.com/developers/applications/me) 79 | - Make a discord bot by clicking on the Bot menu, and click "Add Bot". Name it as you like 80 | - Click "Reset Token" to reveal the bot token. Make note of this token, you need it in the next step 81 | - In the config.yml, change the bot name and token to your bot token 82 | - Go to [Hugginface Tokens](https://huggingface.co/settings/tokens) (make an account if you don't have one yet) and create a token 83 | - Replace `token_from_hf` in config.yml with the token you just created 84 | - This token allows you access to the Stable Diffusion model! 85 | - Save the config 86 | - Go to the OAuth2 menu, and click the URL Generator submenu. Check off the Bot checkbox 87 | - In bot permissions, you only need the following: 88 | ![Bot permissions checkboxes](./extras/bot_perms.png) 89 | - After you have done this, you can copy the link and invite the bot into your server 90 | - The first time, the bot may be triggering a timeout error, this is because it has to download all the model files. After it's done, it'll run properly 91 | - You can now resume the previous steps (wherever you were forwarded from) 92 | 93 | ### How to enable the Share feature (optional) 94 | 95 | - [Make sure Discord is in developer mode](https://www.howtogeek.com/714348/how-to-enable-or-disable-developer-mode-on-discord) 96 | - Make a channel, any place you like (bot needs to be able to post to it, so make sure has write permissions there). Name it something like `gallery` 97 | - Right-click on the channel and click "Copy ID" 98 | - Paste the channel ID in `shareChannelID` in the bots `config.yml`. Restart bot if its running! 99 | - Now you and your members can use `/share` for showcasing your fine art! 100 | 101 | # Sister projects 102 | 103 | These are co-developed with Thingy! 104 | 105 | - [Keep My JCloud](https://github.com/peterwilli/KeepMyJCloud) 106 | - [LEAP](https://github.com/peterwilli/sd-leap-booster) 107 | 108 | # Changelog 109 | 110 | - 31 mar 2023: Deliberate 2 port + New sponsor for the bot 111 | - 10 jan 2023: Added LEAP! 112 | - 01 dec 2022: Uploaded new Docker image for Thingy 3 + fixed sharing + fixed readme instructions 113 | - 26 sept 2022: Added Keep My JCloud as sister project, integrated in the bot using Docker. It keeps JCloud instances running by re-deploying if it dissapears. 114 | - 21 oct 2022: Fixes for hanging cache system + sister projects updates + faster SD! + Magic Prompts! -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | import com.google.protobuf.gradle.* 3 | 4 | plugins { 5 | kotlin("jvm") version "1.6.21" 6 | application 7 | id("com.google.protobuf") version "0.8.19" 8 | id("com.github.johnrengelman.shadow") version "7.1.2" 9 | } 10 | 11 | group = "org.thingy" 12 | version = "2.0" 13 | 14 | repositories { 15 | mavenCentral() 16 | maven("https://jitpack.io") 17 | } 18 | 19 | dependencies { 20 | testImplementation(kotlin("test")) 21 | implementation("com.github.minndevelopment:jda-ktx:17eb77a") 22 | implementation("net.dv8tion:JDA:5.0.0-beta.3") 23 | implementation("ch.qos.logback:logback-classic:1.4.5") 24 | implementation("io.grpc:grpc-kotlin-stub:1.3.0") 25 | implementation("io.grpc:grpc-protobuf:1.51.0") 26 | implementation("com.google.protobuf:protobuf-kotlin:3.21.9") 27 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") 28 | implementation("io.grpc:grpc-netty:1.51.0") 29 | implementation("com.beust:klaxon:5.6") 30 | implementation("com.sksamuel.hoplite:hoplite-core:2.6.5") 31 | implementation("com.sksamuel.hoplite:hoplite-yaml:2.6.5") 32 | implementation("com.j256.ormlite:ormlite-core:6.1") 33 | implementation("com.j256.ormlite:ormlite-jdbc:6.1") 34 | implementation("redis.clients:jedis:4.3.2") 35 | implementation("org.xerial:sqlite-jdbc:3.40.0.0") 36 | implementation("org.atteo:evo-inflector:1.3") 37 | implementation("com.google.code.gson:gson:2.10") 38 | implementation("org.apache.commons:commons-lang3:3.12.0") 39 | implementation("org.apache.directory.studio:org.apache.commons.io:2.4") 40 | implementation("com.google.protobuf:protobuf-java-util:3.21.9") 41 | implementation("net.coobird:thumbnailator:0.4.18") 42 | implementation(kotlin("reflect")) 43 | } 44 | 45 | tasks.test { 46 | useJUnitPlatform() 47 | } 48 | 49 | tasks.withType { 50 | kotlinOptions.jvmTarget = "18" 51 | } 52 | 53 | application { 54 | mainClass.set("MainKt") 55 | } 56 | 57 | tasks.named("shadowJar", com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar::class) { 58 | mergeServiceFiles() 59 | } 60 | 61 | protobuf { 62 | protoc { 63 | artifact = "com.google.protobuf:protoc:3.21.5" 64 | } 65 | plugins { 66 | id("grpc") { 67 | artifact = "io.grpc:protoc-gen-grpc-java:1.49.0" 68 | } 69 | id("grpckt") { 70 | artifact = "io.grpc:protoc-gen-grpc-kotlin:1.3.0:jdk8@jar" 71 | } 72 | } 73 | generateProtoTasks { 74 | all().forEach { 75 | it.plugins { 76 | id("grpc") 77 | id("grpckt") 78 | } 79 | it.builtins { 80 | id("kotlin") 81 | } 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /config.example.yml: -------------------------------------------------------------------------------- 1 | bot: 2 | name: "Thingy" 3 | token: "my_bot_token_keep_secret" 4 | hfToken: "token_from_hf" 5 | ownerId: "my_discord_id" 6 | hostConstraints: 7 | maxEntriesPerOwner: 1 8 | totalImagesInMakeCommand: 4 9 | databasePath: "./db.sqlite" 10 | imagesFolder: "./images" 11 | shareChannelID: "my_share_channel_id" 12 | redisHost: redis://redis:6379 -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | bot: 5 | image: peterwilli/thingy:latest 6 | restart: unless-stopped 7 | volumes: 8 | - ./thingy_data:/opt/thingy/data 9 | logging: 10 | driver: "json-file" 11 | options: 12 | max-size: "5m" 13 | depends_on: 14 | redis: 15 | condition: service_healthy 16 | 17 | hydrolane: 18 | # Change this if needed 19 | image: peterwilli/hydrolane:cuda11.7-cudnn8-devel 20 | environment: 21 | REDIS_URL: redis://redis:6379 22 | RUST_LOG: debug 23 | logging: 24 | driver: "json-file" 25 | options: 26 | max-size: "5m" 27 | depends_on: 28 | redis: 29 | condition: service_healthy 30 | deploy: 31 | resources: 32 | reservations: 33 | devices: 34 | - driver: nvidia 35 | count: 1 36 | capabilities: [gpu] 37 | 38 | redis: 39 | image: redis:7-alpine 40 | command: 'redis-server --save "" --appendonly no' 41 | healthcheck: 42 | test: [ "CMD", "redis-cli", "--raw", "incr", "ping" ] -------------------------------------------------------------------------------- /extras/bot_perms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterwilli/Thingy/609f7c069c0fd0ac522f695d99ecdd03164ae031/extras/bot_perms.png -------------------------------------------------------------------------------- /extras/research/UI Dev/IMG_20220903_140054.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterwilli/Thingy/609f7c069c0fd0ac522f695d99ecdd03164ae031/extras/research/UI Dev/IMG_20220903_140054.jpg -------------------------------------------------------------------------------- /extras/research/UI Dev/IMG_20220903_140521.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterwilli/Thingy/609f7c069c0fd0ac522f695d99ecdd03164ae031/extras/research/UI Dev/IMG_20220903_140521.jpg -------------------------------------------------------------------------------- /flow_server.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: grpc 4 | monitoring: true 5 | port: 51001 6 | env: 7 | JINA_LOG_LEVEL: debug 8 | jcloud: 9 | name: thingy 10 | executors: 11 | - name: thingy_queue_executor 12 | uses: jinahub+docker://xog5bvtu/0.1.4 13 | timeout_ready: 1200000 14 | jcloud: 15 | resources: 16 | gpu: 1 17 | cpu: 1f 18 | - name: magic_prompt 19 | uses: jinahub+docker://MagicPromptExecutor/v1.1 20 | jcloud: 21 | resources: 22 | cpu: 1 -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterwilli/Thingy/609f7c069c0fd0ac522f695d99ecdd03164ae031/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-7.3.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "Thingy" -------------------------------------------------------------------------------- /src/main/kotlin/Config.kt: -------------------------------------------------------------------------------- 1 | import java.net.URI 2 | 3 | data class HostConstraints( 4 | val maxEntriesPerOwner: Int, 5 | val totalImagesInMakeCommand: Int 6 | ) 7 | 8 | data class Bot( 9 | val name: String, 10 | val ownerId: String, 11 | val token: String, 12 | val hfToken: String 13 | ) 14 | 15 | data class Config( 16 | val bot: Bot, 17 | val databasePath: String, 18 | val hostConstraints: HostConstraints, 19 | val redisHost: URI = URI.create("redis://127.0.0.1:6379"), 20 | val shareChannelID: String, 21 | val artContestChannelID: String?, 22 | val leaderboardChannelID: String?, 23 | ) -------------------------------------------------------------------------------- /src/main/kotlin/Consts.kt: -------------------------------------------------------------------------------- 1 | import com.google.gson.Gson 2 | import com.google.gson.JsonArray 3 | import com.google.gson.JsonObject 4 | import kotlin.math.pow 5 | import kotlin.random.Random 6 | 7 | const val miniManual = "Start by making one using `/make`, `/deliberate`, or `/stable_diffusion`" 8 | val gson = Gson() 9 | const val defaultNegative = "out of frame, lowres, text, error, cropped, worst quality, low quality, jpeg artifacts, ugly, duplicate, morbid, mutilated, out of frame, extra fingers, mutated hands, poorly drawn hands, poorly drawn face, mutation, deformed, blurry, dehydrated, bad anatomy, bad proportions, extra limbs, cloned face, disfigured, gross proportions, malformed limbs, missing arms, missing legs, extra arms, extra legs, fused fingers, too many fingers, long neck, username, watermark, signature," 10 | val imageModels: HashMap = hashMapOf( 11 | "Stable Diffusion" to "stable_diffusion", 12 | "Kadinsky" to "kadinsky", 13 | "SDXL" to "sd_xl", 14 | "Deliberate" to "deliberate", 15 | "Photon (realistic photos)" to "photon", 16 | "Deep-Floyd IF" to "deep_floyd_if") 17 | 18 | val audioModels: HashMap = hashMapOf( 19 | "Bark" to "bark", 20 | "MusicGen" to "musicgen") 21 | 22 | fun getDeepFloydJsonDefaults(): JsonObject { 23 | val obj = JsonObject() 24 | obj.addProperty("seed", Random.nextInt(0, 2.toDouble().pow(32).toInt())) 25 | obj.addProperty("ar", "1:1") 26 | obj.addProperty("_hf_auth_token", config.bot.hfToken) 27 | obj.addProperty("noise_level", 100) 28 | obj.addProperty("steps", 50) 29 | obj.addProperty("negative_prompt", defaultNegative) 30 | obj.add("embeds", JsonArray(0)) 31 | return obj 32 | } 33 | 34 | fun getKadinskyMakeImageJsonDefaults(): JsonObject { 35 | val obj = JsonObject() 36 | obj.addProperty("seed", Random.nextInt(0, 2.toDouble().pow(32).toInt())) 37 | obj.addProperty("ar", "1:1") 38 | obj.addProperty("_hf_auth_token", config.bot.hfToken) 39 | obj.addProperty("noise_level", 100) 40 | obj.addProperty("guidance_scale", 1) 41 | obj.addProperty("size", 512) 42 | obj.addProperty("steps", 50) 43 | obj.addProperty("negative_prompt", defaultNegative) 44 | return obj 45 | } 46 | 47 | val kadinskyHiddenParameters = arrayOf("embeds", "model", "task") 48 | 49 | fun getSDUpscaleJsonDefaults(): JsonObject { 50 | val obj = JsonObject() 51 | obj.addProperty("seed", Random.nextInt(0, 2.toDouble().pow(32).toInt())) 52 | obj.addProperty("_hf_auth_token", config.bot.hfToken) 53 | obj.addProperty("noise_level", 100) 54 | obj.addProperty("tile_border", 16) 55 | obj.addProperty("tiling_mode", "linear") 56 | obj.addProperty("guidance_scale", 9) 57 | obj.addProperty("original_image_slice", 16) 58 | return obj 59 | } 60 | 61 | fun getBarkAudioDefaults(): JsonObject { 62 | val obj = JsonObject() 63 | obj.addProperty("seed", Random.nextInt(0, 2.toDouble().pow(32).toInt())) 64 | obj.addProperty("_hf_auth_token", config.bot.hfToken) 65 | obj.addProperty("duration", 15) 66 | obj.add("embeds", JsonArray(0)) 67 | return obj 68 | } 69 | 70 | fun getSdJsonDefaults(): JsonObject { 71 | val obj = JsonObject() 72 | obj.addProperty("seed", Random.nextInt(0, 2.toDouble().pow(32).toInt())) 73 | obj.addProperty("ar", "1:1") 74 | obj.addProperty("size", 512) 75 | obj.addProperty("_hf_auth_token", config.bot.hfToken) 76 | obj.addProperty("guidance_scale", 9.0) 77 | obj.addProperty("steps", 25) 78 | obj.addProperty("negative_prompt", "out of frame, lowres, text, error, cropped, worst quality, low quality, jpeg artifacts, ugly, duplicate, morbid, mutilated, out of frame, extra fingers, mutated hands, poorly drawn hands, poorly drawn face, mutation, deformed, blurry, dehydrated, bad anatomy, bad proportions, extra limbs, cloned face, disfigured, gross proportions, malformed limbs, missing arms, missing legs, extra arms, extra legs, fused fingers, too many fingers, long neck, username, watermark, signature,") 79 | obj.add("embeds", JsonArray(0)) 80 | return obj 81 | } 82 | 83 | fun getPhotonJsonDefaults(): JsonObject { 84 | val obj = JsonObject() 85 | obj.addProperty("seed", Random.nextInt(0, 2.toDouble().pow(32).toInt())) 86 | obj.addProperty("ar", "1:1") 87 | obj.addProperty("size", 768) 88 | obj.addProperty("_hf_auth_token", config.bot.hfToken) 89 | obj.addProperty("guidance_scale", 6.0) 90 | obj.addProperty("steps", 20) 91 | obj.addProperty("negative_prompt", "out of frame, lowres, text, error, cropped, worst quality, low quality, jpeg artifacts, ugly, duplicate, morbid, mutilated, out of frame, extra fingers, mutated hands, poorly drawn hands, poorly drawn face, mutation, deformed, blurry, dehydrated, bad anatomy, bad proportions, extra limbs, cloned face, disfigured, gross proportions, malformed limbs, missing arms, missing legs, extra arms, extra legs, fused fingers, too many fingers, long neck, username, watermark, signature,") 92 | obj.add("embeds", JsonArray(0)) 93 | return obj 94 | } 95 | 96 | val sdHiddenParameters = arrayOf("embeds", "model") 97 | -------------------------------------------------------------------------------- /src/main/kotlin/client/Test.kt: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import com.google.gson.JsonArray 4 | import com.google.gson.JsonObject 5 | import database.models.ChapterEntry 6 | import kotlinx.coroutines.async 7 | import kotlinx.coroutines.coroutineScope 8 | import redis.clients.jedis.Jedis 9 | 10 | suspend fun main() { 11 | val client = QueueClient(Jedis("localhost", 6379)) 12 | coroutineScope { 13 | async { 14 | client.checkLoop() 15 | } 16 | 17 | val paramsTest = JsonArray() 18 | val param = JsonObject() 19 | param.addProperty("test", "lol") 20 | paramsTest.add(param) 21 | // val entry = QueueEntry("stuff", "peter", paramsTest, JsonObject(), arrayOf(), null, null, ChapterEntry.Companion.Type.Image, ChapterEntry.Companion.Visibility.Private, "png", arrayListOf("test"), false) 22 | // client.uploadEntry(entry) 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/kotlin/commands/art_contest/Leaderboard.kt: -------------------------------------------------------------------------------- 1 | package commands.art_contest 2 | 3 | import config 4 | import database.artContestEntryDao 5 | import database.artContestVoteDao 6 | import database.models.ArtContestEntry 7 | import database.userDao 8 | import dev.minn.jda.ktx.events.listener 9 | import kotlinx.coroutines.runBlocking 10 | import net.dv8tion.jda.api.JDA 11 | import net.dv8tion.jda.api.events.session.ReadyEvent 12 | import org.slf4j.LoggerFactory 13 | import utils.sanitize 14 | import java.time.LocalTime 15 | import java.util.concurrent.Executors 16 | import java.util.concurrent.TimeUnit 17 | import kotlin.reflect.KClass 18 | 19 | var lastHour = 0 20 | 21 | fun getTopN(max: Int): Array> { 22 | var lastEntryID: Long = 0 23 | val result = mutableListOf>() 24 | while (true) { 25 | val entry = 26 | artContestEntryDao.queryBuilder().selectColumns().where().ge("id", lastEntryID).queryForFirst() ?: break 27 | val votesCount = artContestVoteDao.queryBuilder().where().eq("contestEntryID", entry.id).countOf() 28 | result.add(entry to votesCount) 29 | if (result.size > max) { 30 | result.sortByDescending { 31 | it.second 32 | } 33 | result.removeLast() 34 | } 35 | lastEntryID = entry.id + 1 36 | } 37 | result.sortByDescending { 38 | it.second 39 | } 40 | return result.toTypedArray() 41 | } 42 | 43 | fun sendLeaderboard(jda: JDA) { 44 | runBlocking { 45 | syncVotes(jda) 46 | } 47 | val leaderboard = StringBuilder() 48 | leaderboard.append("**Leaderboard!**\n") 49 | val topNEntries = getTopN(10) 50 | for ((i, entry) in topNEntries.withIndex()) { 51 | val user = userDao.queryBuilder().selectColumns("discordUserID").where() 52 | .eq("id", entry.first.userID).queryForFirst() 53 | val discordUser = jda.retrieveUserById(user.discordUserID!!).complete() 54 | val place = when (i) { 55 | 0 -> { 56 | ":first_place:" 57 | } 58 | 59 | 1 -> { 60 | ":second_place:" 61 | } 62 | 63 | 2 -> { 64 | ":third_place:" 65 | } 66 | 67 | else -> { 68 | "#${i + 1}" 69 | } 70 | } 71 | leaderboard.append("$place (${entry.second} votes): *${sanitize(entry.first.prompt)}* by **${discordUser.name}**: ${entry.first.messageLink}\n") 72 | } 73 | val channel = jda.getTextChannelById(config.leaderboardChannelID!!)!! 74 | channel.sendMessage(leaderboard.toString()).queue() 75 | } 76 | 77 | fun leaderboardScheduler(jda: JDA, triggerHour: Int) { 78 | val logger = LoggerFactory.getLogger(KClass::class.java) 79 | jda.listener { _ -> 80 | logger.debug("leaderboardTimer scheduler turned on") 81 | val dayScheduler = Executors.newScheduledThreadPool(1) 82 | dayScheduler.scheduleAtFixedRate( 83 | { 84 | try { 85 | val time = LocalTime.now() 86 | logger.debug("leaderboardTimer, hour: ${time.hour}") 87 | if (lastHour != time.hour && time.hour == triggerHour) { 88 | sendLeaderboard(jda) 89 | } 90 | lastHour = time.hour 91 | } catch (e: Exception) { 92 | logger.error("leaderboardTimer Error! (ignored)") 93 | e.printStackTrace() 94 | } 95 | }, 96 | 0, 97 | TimeUnit.HOURS.toSeconds(1), 98 | TimeUnit.SECONDS 99 | ) 100 | } 101 | } -------------------------------------------------------------------------------- /src/main/kotlin/commands/art_contest/RemoveFromContestCommand.kt: -------------------------------------------------------------------------------- 1 | package commands.art_contest 2 | 3 | import GetPageCallback 4 | import Paginator 5 | import config 6 | import database.artContestEntryDao 7 | import database.models.ArtContestEntry 8 | import database.userDao 9 | import dev.minn.jda.ktx.events.onCommand 10 | import dev.minn.jda.ktx.interactions.components.button 11 | import dev.minn.jda.ktx.messages.MessageCreate 12 | import dev.minn.jda.ktx.messages.editMessage 13 | import dev.minn.jda.ktx.messages.reply_ 14 | import editMessageToIncludePaginator 15 | import getAverageColor 16 | import miniManual 17 | import net.dv8tion.jda.api.EmbedBuilder 18 | import net.dv8tion.jda.api.JDA 19 | import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle 20 | import net.dv8tion.jda.api.utils.FileUpload 21 | import paginator 22 | import utils.bufferedImageToByteArray 23 | import utils.sendException 24 | import java.net.URL 25 | import javax.imageio.ImageIO 26 | import kotlin.time.Duration.Companion.minutes 27 | 28 | fun makeSelectArtContestEntry(entries: List): Paginator { 29 | val getPageCallback: GetPageCallback = { index -> 30 | val page = EmbedBuilder() 31 | val entry = entries[index.toInt()] 32 | page.setTitle(entry.prompt.take(255)) 33 | page.setFooter("Entry ${index + 1} / ${entries.size}") 34 | page.setImage("attachment://${index + 1}.jpg") 35 | val image = ImageIO.read(URL(entry.imageURL)) 36 | val avgColor = getAverageColor(image, 0, 0, image.width, image.height) 37 | page.setColor(avgColor) 38 | page.build() 39 | val msgCreate = MessageCreate( 40 | embeds = listOf(page.build()), 41 | files = listOf(FileUpload.fromData(bufferedImageToByteArray(image, "jpg"), "${index + 1}.jpg")) 42 | ) 43 | msgCreate 44 | } 45 | val imageSelector = 46 | paginator(amountOfPages = entries.size.toLong(), getPage = getPageCallback, expireAfter = 10.minutes) 47 | imageSelector.injectMessageCallback = { index, msgEdit -> 48 | val entry = entries[index.toInt()] 49 | val image = ImageIO.read(URL(entry.imageURL)) 50 | msgEdit.setFiles(FileUpload.fromData(bufferedImageToByteArray(image, "jpg"), "${index + 1}.jpg")) 51 | } 52 | return imageSelector 53 | } 54 | 55 | fun removeFromContestCommand(jda: JDA) { 56 | jda.onCommand("remove_from_contest") { event -> 57 | try { 58 | val user = userDao.queryBuilder().selectColumns("id", "currentChapterId").where() 59 | .eq("discordUserID", event.user.id).queryForFirst() 60 | if (user == null) { 61 | event.reply_("User '${event.user.id}' not found! Did you make art yet? $miniManual") 62 | .setEphemeral(true).queue() 63 | return@onCommand 64 | } 65 | val memberEntries = artContestEntryDao.queryBuilder().selectColumns().where().eq("userID", user.id).query() 66 | if (memberEntries.isEmpty()) { 67 | event.reply_("No art contest entries found yet! Want to participate? Run `/submit_to_contest` after making some art!") 68 | .setEphemeral(true).queue() 69 | return@onCommand 70 | } 71 | event.deferReply().setEphemeral(true).queue() 72 | val imageSelector = makeSelectArtContestEntry(memberEntries) 73 | imageSelector.customActionComponents = listOf(jda.button( 74 | label = "Delete", 75 | style = ButtonStyle.DANGER, 76 | user = event.user 77 | ) { btnEvent -> 78 | btnEvent.reply_("Are you sure to delete this entry from the contest? **You will also lose your votes and cannot be undone!**") 79 | .setEphemeral(true).addActionRow(listOf( 80 | jda.button( 81 | label = "Delete!", 82 | style = ButtonStyle.DANGER, 83 | user = event.user 84 | ) { 85 | val memberEntryToDelete = memberEntries[imageSelector.getIndex().toInt()] 86 | val channel = jda.getTextChannelById(config.artContestChannelID!!) 87 | if (channel == null) { 88 | btnEvent.hook.editMessage(content = "Can't find art channel! Art entry not deleted.") 89 | .setComponents().queue() 90 | } else { 91 | val messageId = memberEntryToDelete.messageLink.split("/").last() 92 | channel.deleteMessageById(messageId).queue() 93 | memberEntryToDelete.delete() 94 | btnEvent.hook.editMessage(content = "*Deleted!*").setComponents().queue() 95 | } 96 | }, 97 | jda.button( 98 | label = "Keep!", 99 | style = ButtonStyle.PRIMARY, 100 | user = event.user 101 | ) { 102 | btnEvent.hook.editMessage(content = "*Delete canceled*").setComponents().queue() 103 | } 104 | )).queue() 105 | }) 106 | event.hook.editMessageToIncludePaginator(imageSelector).queue() 107 | } catch (e: Exception) { 108 | e.printStackTrace() 109 | event.sendException(e) 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/kotlin/commands/art_contest/TestLeaderboard.kt: -------------------------------------------------------------------------------- 1 | package commands.art_contest 2 | 3 | import commands.make.standardPermissionList 4 | import commands.make.validatePermissions 5 | import dev.minn.jda.ktx.events.onCommand 6 | import dev.minn.jda.ktx.messages.reply_ 7 | import kotlinx.coroutines.delay 8 | import net.dv8tion.jda.api.JDA 9 | 10 | fun testLeaderboardCommand(jda: JDA) { 11 | jda.onCommand("test_leaderboard") { event -> 12 | if (!validatePermissions(event, standardPermissionList)) { 13 | return@onCommand 14 | } 15 | event.reply_("Test is starting in a few seconds...").setEphemeral(true).queue() 16 | delay(2000) 17 | sendLeaderboard(event.jda) 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/commands/art_contest/Vote.kt: -------------------------------------------------------------------------------- 1 | package commands.art_contest 2 | 3 | import com.j256.ormlite.misc.TransactionManager 4 | import config 5 | import database.artContestEntryDao 6 | import database.artContestVoteDao 7 | import database.connectionSource 8 | import database.models.ArtContestVote 9 | import dev.minn.jda.ktx.coroutines.await 10 | import dev.minn.jda.ktx.events.listener 11 | import kotlinx.coroutines.runBlocking 12 | import net.dv8tion.jda.api.JDA 13 | import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent 14 | import net.dv8tion.jda.api.events.message.react.MessageReactionRemoveEvent 15 | 16 | private const val MAX_VOTES = 3 17 | 18 | private suspend fun removeTooManyVotes(jda: JDA) { 19 | val channel = jda.getTextChannelById(config.artContestChannelID!!)!! 20 | val usersCount = mutableMapOf() 21 | for (message in channel.iterableHistory) { 22 | for (reaction in message.reactions) { 23 | val users = reaction.retrieveUsers().await() 24 | for(user in users) { 25 | if(user.isBot) { 26 | continue 27 | } 28 | val currentCount = usersCount.getOrDefault(user.id, 0) 29 | usersCount[user.id] = currentCount + 1 30 | if (usersCount[user.id]!! > MAX_VOTES) { 31 | reaction.removeReaction(user).await() 32 | println("[removeTooManyVotes] Removing ${user.name} because they are above MAX_VOTES ($MAX_VOTES)") 33 | } 34 | } 35 | } 36 | } 37 | } 38 | 39 | suspend fun syncVotes(jda: JDA) { 40 | removeTooManyVotes(jda) 41 | TransactionManager.callInTransaction(connectionSource) { 42 | val votesDelete = artContestVoteDao.deleteBuilder() 43 | artContestVoteDao.delete(votesDelete.prepare()) 44 | 45 | runBlocking { 46 | val channel = jda.getTextChannelById(config.artContestChannelID!!)!! 47 | for (message in channel.iterableHistory) { 48 | val entry = artContestEntryDao.queryBuilder().selectColumns("id").where().eq("messageID", message.idLong) 49 | .queryForFirst() 50 | for (reaction in message.reactions) { 51 | val users = reaction.retrieveUsers().await() 52 | for(user in users) { 53 | if(user.isBot) { 54 | continue 55 | } 56 | val vote = 57 | ArtContestVote( 58 | user!!.idLong, entry.id 59 | ) 60 | artContestVoteDao.create(vote) 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | 68 | fun voteReactionWatcher(jda: JDA) { 69 | jda.listener { 70 | if (it.user!!.isBot) { 71 | return@listener 72 | } 73 | val artContestChannel = jda.getTextChannelById(config.artContestChannelID!!)!! 74 | if (it.channel != artContestChannel) { 75 | return@listener 76 | } 77 | TransactionManager.callInTransaction(connectionSource) { 78 | val previousVotes = 79 | artContestVoteDao.queryBuilder().selectColumns().where().eq("userID", it.user!!.idLong).query() 80 | if (previousVotes.size >= MAX_VOTES) { 81 | for (i in 0..(previousVotes.size - MAX_VOTES)) { 82 | val voteToDelete = previousVotes[i] 83 | val entryVotedOn = 84 | artContestEntryDao.queryBuilder().selectColumns().where().eq("id", voteToDelete.contestEntryID) 85 | .queryForFirst() 86 | voteToDelete.delete() 87 | val message = artContestChannel.retrieveMessageById(entryVotedOn.messageID).complete() 88 | message.removeReaction(defaultVoteEmoji, it.user!!).queue() 89 | } 90 | } 91 | val entry = artContestEntryDao.queryBuilder().selectColumns("id").where().eq("messageID", it.messageIdLong) 92 | .queryForFirst() 93 | 94 | val vote = 95 | ArtContestVote( 96 | it.user!!.idLong, entry.id 97 | ) 98 | artContestVoteDao.create(vote) 99 | } 100 | } 101 | 102 | jda.listener { 103 | if (it.user!!.isBot) { 104 | return@listener 105 | } 106 | val artContestChannel = jda.getTextChannelById(config.artContestChannelID!!)!! 107 | if (it.channel != artContestChannel) { 108 | return@listener 109 | } 110 | TransactionManager.callInTransaction(connectionSource) { 111 | val entry = artContestEntryDao.queryBuilder().selectColumns("id").where().eq("messageID", it.messageIdLong) 112 | .queryForFirst() 113 | artContestVoteDao.queryBuilder().selectColumns().where().eq("userID", it.user!!.idLong).and() 114 | .eq("contestEntryID", entry.id).queryForFirst()?.delete() 115 | } 116 | } 117 | } -------------------------------------------------------------------------------- /src/main/kotlin/commands/art_contest/cancel/CancelCommand.kt: -------------------------------------------------------------------------------- 1 | package commands.art_contest.cancel 2 | 3 | import dev.minn.jda.ktx.events.onCommand 4 | import dev.minn.jda.ktx.messages.reply_ 5 | import net.dv8tion.jda.api.JDA 6 | import queueClient 7 | 8 | fun cancelCommand(jda: JDA) { 9 | jda.onCommand("cancel") { event -> 10 | queueClient.cancelLastEntryByOwner(event.user.id) 11 | event.reply_("The last item by you is deleted, if you had at least 1 item in the queue!").queue() 12 | } 13 | } -------------------------------------------------------------------------------- /src/main/kotlin/commands/chapters/ListChaptersCommand.kt: -------------------------------------------------------------------------------- 1 | package commands.chapters 2 | 3 | import com.google.gson.JsonArray 4 | import com.j256.ormlite.misc.TransactionManager 5 | import database.chapterDao 6 | import database.connectionSource 7 | import database.models.ChapterEntry 8 | import database.models.UserChapter 9 | import database.userDao 10 | import dev.minn.jda.ktx.events.onCommand 11 | import dev.minn.jda.ktx.interactions.components.button 12 | import dev.minn.jda.ktx.messages.editMessage 13 | import dev.minn.jda.ktx.messages.reply_ 14 | import gson 15 | import miniManual 16 | import net.dv8tion.jda.api.JDA 17 | import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle 18 | import replyPaginator 19 | import ui.GetImageCallback 20 | import ui.ImageSliderEntry 21 | import ui.sendImageSlider 22 | import utils.sanitize 23 | import java.net.URL 24 | 25 | fun listChaptersCommand(jda: JDA) { 26 | jda.onCommand("chapters") { event -> 27 | try { 28 | val user = 29 | userDao.queryBuilder().selectColumns("id").where().eq("discordUserID", event.user.id).queryForFirst() 30 | if (user == null) { 31 | event.reply_("User not found! Did you make art yet? $miniManual") 32 | .setEphemeral(true).queue() 33 | return@onCommand 34 | } 35 | 36 | val chapterType = if(event.getOption("type") == null) { 37 | "images" 38 | } 39 | else { 40 | event.getOption("type")!!.asString 41 | } 42 | 43 | if (chapterType == "images") { 44 | val chaptersCount = 45 | chapterDao.queryBuilder().selectColumns("id").where() 46 | .eq("userID", user.id) 47 | .and().eq("chapterType", ChapterEntry.Companion.Type.Image.ordinal).countOf() 48 | if (chaptersCount == 0L) { 49 | event.reply_("Sorry, we couldn't find any chapters! $miniManual") 50 | .setEphemeral(true).queue() 51 | return@onCommand 52 | } 53 | var lastSelectedChapter: UserChapter? = null 54 | val onImage: GetImageCallback = { index -> 55 | lastSelectedChapter = 56 | chapterDao.queryBuilder().selectColumns().limit(1).offset(index).orderBy("creationTimestamp", false) 57 | .where() 58 | .eq("userID", user.id) 59 | .and().eq("chapterType", ChapterEntry.Companion.Type.Image.ordinal) 60 | .queryForFirst() 61 | 62 | val latestEntry = lastSelectedChapter!!.getLatestEntry() 63 | val description = latestEntry.getDescription() 64 | ImageSliderEntry( 65 | description = description, 66 | image = URL(latestEntry.data) 67 | ) 68 | } 69 | val slider = sendImageSlider("My Chapters", chaptersCount, onImage) 70 | slider.customActionComponents = listOf(jda.button( 71 | label = "Select", 72 | style = ButtonStyle.PRIMARY, 73 | user = event.user 74 | ) { 75 | val parameters = 76 | gson.fromJson( 77 | lastSelectedChapter!!.getLatestEntry().parameters, 78 | JsonArray::class.java 79 | ) 80 | user.updateSelectedChapter(lastSelectedChapter!!.id) 81 | it.reply_( 82 | "${ 83 | sanitize(parameters[0].asJsonObject.get("prompt").asString) 84 | } is now your current chapter! You can use editing commands such as `/upscale`, `/variate` to edit it! Enjoy!" 85 | ).setEphemeral(true).queue() 86 | }, jda.button( 87 | label = "\uD83D\uDDD1️", 88 | style = ButtonStyle.DANGER, 89 | user = event.user 90 | ) { deleteEvent -> 91 | deleteEvent.reply_( 92 | "**Are you sure to delete this chapter?** *${ 93 | sanitize( 94 | lastSelectedChapter!!.getLatestEntry().getDescription() 95 | ) 96 | }*" 97 | ) 98 | .setEphemeral(true).addActionRow(listOf( 99 | jda.button( 100 | label = "Delete!", 101 | style = ButtonStyle.DANGER, 102 | user = event.user 103 | ) { 104 | lastSelectedChapter!!.delete() 105 | TransactionManager.callInTransaction(connectionSource) { 106 | // If we don't have a chapter selected anymore we likely deleted a selected chapter. 107 | val usingChapter = 108 | chapterDao.queryBuilder().selectColumns().where().eq("id", user.currentChapterId) 109 | .and() 110 | .eq("userID", user.id).queryForFirst() 111 | if (usingChapter == null) { 112 | val possibleLastChapter = 113 | chapterDao.queryBuilder().selectColumns("id").limit(1) 114 | .orderBy("creationTimestamp", false).where().eq("userID", user.id) 115 | .queryForFirst() 116 | if (possibleLastChapter != null) { 117 | user.updateSelectedChapter(possibleLastChapter.id) 118 | } 119 | } 120 | } 121 | deleteEvent.hook.editMessage(content = "*Deleted!*").setComponents().queue() 122 | }, 123 | jda.button( 124 | label = "Keep!", 125 | style = ButtonStyle.PRIMARY, 126 | user = event.user 127 | ) { 128 | deleteEvent.hook.editMessage(content = "*Delete canceled*").setComponents().queue() 129 | } 130 | )).queue() 131 | }) 132 | event.replyPaginator(slider).setEphemeral(true).queue() 133 | } 134 | } catch (e: Exception) { 135 | e.printStackTrace() 136 | event.reply_("**Error!** $e").setEphemeral(true).queue() 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /src/main/kotlin/commands/chapters/RollbackCommand.kt: -------------------------------------------------------------------------------- 1 | package commands.chapters 2 | 3 | import com.google.gson.JsonArray 4 | import database.chapterDao 5 | import database.chapterEntryDao 6 | import database.models.ChapterEntry 7 | import database.userDao 8 | import dev.minn.jda.ktx.events.onCommand 9 | import dev.minn.jda.ktx.interactions.components.button 10 | import dev.minn.jda.ktx.messages.reply_ 11 | import gson 12 | import miniManual 13 | import net.dv8tion.jda.api.JDA 14 | import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle 15 | import replyPaginator 16 | import ui.GetImageCallback 17 | import ui.ImageSliderEntry 18 | import ui.sendImageSlider 19 | import java.net.URL 20 | 21 | fun rollbackChapterCommand(jda: JDA) { 22 | jda.onCommand("rollback") { event -> 23 | try { 24 | val user = 25 | userDao.queryBuilder().selectColumns("id", "currentChapterId").where() 26 | .eq("discordUserID", event.user.id) 27 | .queryForFirst() 28 | if (user == null) { 29 | event.reply_("User '${event.user.id}' not found! Did you make art yet? $miniManual") 30 | .setEphemeral(true).queue() 31 | return@onCommand 32 | } 33 | 34 | val usingChapter = 35 | chapterDao.queryBuilder().selectColumns().where().eq("id", user.currentChapterId).and() 36 | .eq("userID", user.id).queryForFirst() 37 | if (usingChapter == null) { 38 | event.reply_("Sorry, we couldn't find any chapters! $miniManual") 39 | .setEphemeral(true).queue() 40 | return@onCommand 41 | } 42 | 43 | val entryCount = usingChapter.getEntryCount() 44 | var lastEntry: ChapterEntry? = null 45 | val onImage: GetImageCallback = { index -> 46 | lastEntry = usingChapter.getEntryAtIndex(index) 47 | val parameters = gson.fromJson(lastEntry!!.parameters, JsonArray::class.java) 48 | ImageSliderEntry( 49 | description = parameters[0].asJsonObject.get("prompt").asString, 50 | image = URL(lastEntry!!.data) 51 | ) 52 | } 53 | val slider = sendImageSlider("Rollback to", entryCount, onImage) 54 | slider.customActionComponents = listOf(jda.button( 55 | label = "Rollback", 56 | style = ButtonStyle.PRIMARY, 57 | user = event.user 58 | ) { 59 | val entryToRollBackTo = lastEntry!! 60 | val db = chapterEntryDao.deleteBuilder() 61 | db.where().eq("chapterID", entryToRollBackTo.chapterID).and() 62 | .gt("creationTimestamp", entryToRollBackTo.creationTimestamp) 63 | chapterEntryDao.delete(db.prepare()) 64 | it.reply_("Rolled back! You can use editing commands such as `/upscale`, `/variate` to edit it! Enjoy!") 65 | .setEphemeral(true).queue() 66 | }) 67 | event.replyPaginator(slider).setEphemeral(true).queue() 68 | } catch (e: Exception) { 69 | e.printStackTrace() 70 | event.reply_("**Error!** $e").setEphemeral(true).queue() 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /src/main/kotlin/commands/edit/EditImageCommand.kt: -------------------------------------------------------------------------------- 1 | import com.google.gson.JsonArray 2 | import com.google.gson.JsonObject 3 | import commands.make.standardPermissionList 4 | import commands.make.validatePermissions 5 | import database.models.ChapterEntry 6 | import dev.minn.jda.ktx.coroutines.await 7 | import dev.minn.jda.ktx.events.onCommand 8 | import dev.minn.jda.ktx.messages.reply_ 9 | import net.dv8tion.jda.api.JDA 10 | import net.dv8tion.jda.api.interactions.InteractionHook 11 | import utils.* 12 | import java.io.ByteArrayInputStream 13 | import java.io.ByteArrayOutputStream 14 | import javax.imageio.ImageIO 15 | import kotlin.math.max 16 | import kotlin.math.pow 17 | import kotlin.random.Random 18 | 19 | fun getEditImageJsonDefaults(): JsonObject { 20 | val obj = JsonObject() 21 | obj.addProperty("seed", Random.nextInt(0, 2.toDouble().pow(32).toInt())) 22 | obj.addProperty("_hf_auth_token", config.bot.hfToken) 23 | obj.addProperty("steps", 20) 24 | obj.addProperty("guidance_scale", 9.0) 25 | obj.addProperty("input_scale", 1.0) 26 | return obj 27 | } 28 | 29 | fun editImageCommand(jda: JDA) { 30 | jda.onCommand("edit_image") { event -> 31 | event.reply_("Todo!").setEphemeral(true).queue() 32 | /* 33 | try { 34 | if (!validatePermissions(event, standardPermissionList)) { 35 | return@onCommand 36 | } 37 | event.deferReply().await() 38 | val attachment = event.getOption("image")!!.asAttachment 39 | if (!attachment.isImage) { 40 | event.reply_( 41 | "Only images are supported!" 42 | ).setEphemeral(true).queue() 43 | return@onCommand 44 | } 45 | val defaults = getEditImageJsonDefaults() 46 | val params = event.optionsToJson { 47 | var image = ImageIO.read(ByteArrayInputStream(it)) 48 | image = image.upscaleToMinSize(768) 49 | image = image.resizeKeepRatio(768, true) 50 | if (max(image.width, image.height) > 1024) { 51 | image = image.resizeKeepRatio(768, false) 52 | } 53 | val baos = ByteArrayOutputStream() 54 | ImageIO.write(image, "jpg", baos) 55 | baos.toByteArray() 56 | }.withDefaults(defaults) 57 | params.addProperty("original_url", attachment.url) 58 | defaults.addProperty("seed", 0) 59 | 60 | fun createEntry(hook: InteractionHook, params: JsonObject): FairQueueEntry { 61 | val batch = JsonArray() 62 | for (idx in 0 until config.hostConstraints.totalImagesInMakeCommand) { 63 | val clonedParams = params.deepCopy() 64 | val seed = clonedParams["seed"].asLong 65 | clonedParams.addProperty("seed", seed + idx) 66 | batch.add(clonedParams) 67 | } 68 | return FairQueueEntry( 69 | "Editing image", 70 | event.member!!.id, 71 | batch, 72 | defaults, 73 | sdHiddenParameters, 74 | "edit_image", 75 | hook, 76 | null, 77 | ChapterEntry.Companion.Type.Image, 78 | ChapterEntry.Companion.Visibility.Public, 79 | "jpg" 80 | ) 81 | } 82 | 83 | val entry = createEntry(event.hook, params) 84 | // //queueDispatcher.queue.addToQueue(entry) 85 | } catch (e: Exception) { 86 | e.printStackTrace() 87 | event.sendException(e) 88 | } 89 | */ 90 | } 91 | } -------------------------------------------------------------------------------- /src/main/kotlin/commands/edit/RemoveBackgroundImageCommand.kt: -------------------------------------------------------------------------------- 1 | import client.QueueEntry 2 | import com.google.gson.JsonArray 3 | import com.google.gson.JsonObject 4 | import commands.make.* 5 | import database.models.ChapterEntry 6 | import dev.minn.jda.ktx.coroutines.await 7 | import dev.minn.jda.ktx.events.onCommand 8 | import net.dv8tion.jda.api.JDA 9 | import net.dv8tion.jda.api.interactions.InteractionHook 10 | import utils.* 11 | 12 | fun removeBackgroundImageCommand(jda: JDA) { 13 | jda.onCommand("remove_background_from_image") { event -> 14 | try { 15 | if (!validatePermissions(event, standardPermissionList)) { 16 | return@onCommand 17 | } 18 | event.deferReply().await() 19 | val params = event.optionsToJson() 20 | 21 | fun createEntry(hook: InteractionHook, params: JsonObject): QueueEntry { 22 | val batch = JsonArray() 23 | batch.add(params) 24 | return QueueEntry("Remove background from image", event.member!!.id, 25 | batch, 26 | JsonObject(), 27 | arrayOf(), 28 | hook, 29 | null, 30 | ChapterEntry.Companion.Type.Image, 31 | ChapterEntry.Companion.Visibility.Public, 32 | "png", arrayOf("remove_bg_image") 33 | ) 34 | } 35 | val entry = createEntry(event.hook, params) 36 | queueClient.uploadEntry(entry) 37 | } catch (e: Exception) { 38 | e.printStackTrace() 39 | event.sendException(e) 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/kotlin/commands/edit/UpscaleImageCommand.kt: -------------------------------------------------------------------------------- 1 | import client.QueueEntry 2 | import com.google.gson.JsonArray 3 | import database.chapterDao 4 | import database.models.ChapterEntry 5 | import database.userDao 6 | import dev.minn.jda.ktx.coroutines.await 7 | import dev.minn.jda.ktx.events.onCommand 8 | import dev.minn.jda.ktx.messages.editMessage 9 | import dev.minn.jda.ktx.messages.reply_ 10 | import net.dv8tion.jda.api.JDA 11 | import ui.makeSelectImageFromQuilt 12 | import utils.bufferedImageToByteArray 13 | import utils.optionsToJson 14 | import utils.takeSlice 15 | import utils.withDefaults 16 | import java.net.URL 17 | import java.util.* 18 | import javax.imageio.ImageIO 19 | 20 | fun upscaleImageCommand(jda: JDA) { 21 | jda.onCommand("upscale") { event -> 22 | event.deferReply(true).queue() 23 | val user = userDao.queryBuilder().selectColumns("id", "currentChapterId").where() 24 | .eq("discordUserID", event.user.id).queryForFirst() 25 | 26 | if (user == null) { 27 | event.reply_("User '${event.user.id}' not found! Did you make art yet? $miniManual") 28 | .setEphemeral(true).queue() 29 | return@onCommand 30 | } 31 | 32 | val usingChapter = 33 | chapterDao.queryBuilder().selectColumns().where() 34 | .eq("id", user.currentChapterId).and().eq("userID", user.id).queryForFirst() 35 | if (usingChapter == null) { 36 | event.reply_("Sorry, we couldn't find any chapters! $miniManual") 37 | .setEphemeral(true).queue() 38 | return@onCommand 39 | } 40 | 41 | if (usingChapter.chapterType == ChapterEntry.Companion.Type.Image.ordinal) { 42 | val latestEntry = usingChapter.getLatestEntry() 43 | val image = ImageIO.read(URL(latestEntry.data)) 44 | val parameters = gson.fromJson(latestEntry.parameters, JsonArray::class.java) 45 | val quiltSelector = makeSelectImageFromQuilt( 46 | event.user, 47 | "Select image for upscaling", 48 | image, 49 | parameters.size() 50 | ) { _, chosenImage -> 51 | event.hook.editMessage(content = "Processing image... Please wait...", components = listOf(), embeds = listOf()).await() 52 | val imageSlice = takeSlice(image, parameters.size(), chosenImage) 53 | val imageB64 = Base64.getEncoder().encodeToString(bufferedImageToByteArray(imageSlice, "png")) 54 | val batch = JsonArray() 55 | val params = event.optionsToJson().withDefaults(getSDUpscaleJsonDefaults()) 56 | params.addProperty("image", imageB64) 57 | val maybeOverride = event.getOption("prompt_override") 58 | if(maybeOverride == null) { 59 | val parsedParameters = gson.fromJson(parameters, JsonArray::class.java) 60 | val prompt = parsedParameters[chosenImage].asJsonObject["prompt"].asString 61 | params.addProperty("prompt", prompt) 62 | } else { 63 | params.addProperty("prompt", maybeOverride.asString) 64 | params.remove("prompt_override") 65 | } 66 | batch.add(params) 67 | val entry = QueueEntry( 68 | "Upscale Image", event.member!!.id, 69 | batch, 70 | getSDUpscaleJsonDefaults(), 71 | sdHiddenParameters, 72 | event.hook, 73 | null, 74 | ChapterEntry.Companion.Type.Image, 75 | ChapterEntry.Companion.Visibility.Public, 76 | "jpg", arrayOf("stable_diffusion_upscale") 77 | ) 78 | queueClient.uploadEntry(entry) 79 | } 80 | event.hook.editMessageToIncludePaginator(quiltSelector).queue() 81 | } else { 82 | event.hook.editOriginal("Sorry, you can't upscale a ${usingChapter.chapterType}! Use `/chapters` to select an image chapter!") 83 | .queue() 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /src/main/kotlin/commands/magic_prompt/MagicPromptCommand.kt: -------------------------------------------------------------------------------- 1 | import dev.minn.jda.ktx.coroutines.await 2 | import dev.minn.jda.ktx.events.onCommand 3 | import dev.minn.jda.ktx.messages.reply_ 4 | import net.dv8tion.jda.api.JDA 5 | import utils.messageToURL 6 | 7 | fun magicPromptCommand(jda: JDA) { 8 | jda.onCommand("magic_prompt") { event -> 9 | event.reply_("TODO!").setEphemeral(true).queue() 10 | // event.deferReply().queue() 11 | 12 | try { 13 | /* 14 | val amount = if (event.getOption("amount") == null) { 15 | 5 16 | } else { 17 | event.getOption("amount")!!.asInt 18 | } 19 | val variation = if (event.getOption("variation") == null) { 20 | 0.3 21 | } else { 22 | event.getOption("variation")!!.asInt / 100.0 23 | } 24 | val result = client.magicPrompt(event.getOption("start")!!.asString, amount, variation) 25 | if (result.isEmpty()) { 26 | event.hook.editOriginal("Failed :(").queue() 27 | event.messageChannel.sendMessage("${event.user.asMention} Magic Prompt failed! Sorry! Please try again later...") 28 | .queue() 29 | } else { 30 | val firstText = result.first() 31 | val magicMessage = event.messageChannel.sendMessage(firstText).await() 32 | for (text in result.sliceArray(1 until result.size)) { 33 | event.messageChannel.sendMessage(text).queue() 34 | } 35 | event.hook.editOriginal("Done! See ${messageToURL(magicMessage)}").queue() 36 | magicMessage.reply_("${event.user.asMention} Your magic prompts are done! We added them as single messages so it's easy to copy paste on phone! They are yours to do with as you wish!") 37 | .queue() 38 | } 39 | */ 40 | } catch (e: Exception) { 41 | e.printStackTrace() 42 | event.hook.editOriginal("Error! $e").queue() 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/main/kotlin/commands/make/MakeAudioCommand.kt: -------------------------------------------------------------------------------- 1 | import client.QueueEntry 2 | import com.google.gson.JsonArray 3 | import com.google.gson.JsonObject 4 | import commands.make.* 5 | import database.models.ChapterEntry 6 | import dev.minn.jda.ktx.coroutines.await 7 | import dev.minn.jda.ktx.events.onCommand 8 | import dev.minn.jda.ktx.messages.reply_ 9 | import net.dv8tion.jda.api.JDA 10 | import net.dv8tion.jda.api.interactions.InteractionHook 11 | import utils.* 12 | import kotlin.math.pow 13 | import kotlin.random.Random 14 | 15 | fun makeAudioCommand(jda: JDA) { 16 | jda.onCommand("make_audio") { event -> 17 | try { 18 | if (!validatePermissions(event, standardPermissionList)) { 19 | return@onCommand 20 | } 21 | event.deferReply().await() 22 | val params = event.optionsToJson().withDefaults(getBarkAudioDefaults()) 23 | fun createEntry(hook: InteractionHook, params: JsonObject, model: String, scripts: Array): QueueEntry { 24 | val batch = JsonArray() 25 | for (idx in 0 until 4) { 26 | val clonedParams = params.deepCopy() 27 | val seed = clonedParams["seed"].asLong 28 | clonedParams.addProperty("seed", seed + idx) 29 | batch.add(clonedParams) 30 | } 31 | return QueueEntry( 32 | "Making audio (${audioModels.getKey(model)})", 33 | event.member!!.id, 34 | batch, 35 | getBarkAudioDefaults(), 36 | arrayOf(), 37 | hook, 38 | null, 39 | ChapterEntry.Companion.Type.Audio, 40 | ChapterEntry.Companion.Visibility.Public, 41 | "mp3", 42 | scripts, 43 | true 44 | ) 45 | } 46 | val maybeModel = event.getOption("model") 47 | val entry = when(val model = maybeModel?.asString ?: "musicgen") { 48 | "musicgen" -> { 49 | createEntry(event.hook, params, model, arrayOf("musicgen")) 50 | } 51 | "bark" -> { 52 | createEntry(event.hook, params, model, arrayOf("bark")) 53 | } 54 | else -> { 55 | event.reply_("Unknown model: $model").queue() 56 | return@onCommand 57 | } 58 | } 59 | queueClient.uploadEntry(entry) 60 | } catch (e: Exception) { 61 | e.printStackTrace() 62 | event.sendException(e) 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /src/main/kotlin/commands/make/MakeCommand.kt: -------------------------------------------------------------------------------- 1 | import com.google.gson.JsonArray 2 | import com.google.gson.JsonObject 3 | import commands.make.* 4 | import database.models.ChapterEntry 5 | import dev.minn.jda.ktx.events.onCommand 6 | import dev.minn.jda.ktx.messages.reply_ 7 | import kotlinx.coroutines.runBlocking 8 | import net.dv8tion.jda.api.JDA 9 | import net.dv8tion.jda.api.interactions.InteractionHook 10 | import utils.* 11 | import kotlin.random.Random 12 | 13 | fun makeCommand(jda: JDA) { 14 | jda.onCommand("make") { event -> 15 | event.reply_("TODO!").setEphemeral(true).queue() 16 | /* 17 | try { 18 | if (!validatePermissions(event, standardPermissionList)) { 19 | return@onCommand 20 | } 21 | event.deferReply().queue() 22 | val client = jcloudClient.currentClient() 23 | val prompt = event.getOption("prompt")!!.asString 24 | val params = event.optionsToJson().withDefaults(getSdJsonDefaults()) 25 | 26 | suspend fun createEntry(hook: InteractionHook, params: JsonObject): FairQueueEntry { 27 | val magicPromptResult = 28 | client.magicPrompt(prompt, config.hostConstraints.totalImagesInMakeCommand - 1, Random.nextDouble()) 29 | 30 | var batch = JsonArray() 31 | for (idx in 0 until config.hostConstraints.totalImagesInMakeCommand) { 32 | val newParams = params.deepCopy() 33 | val seed = newParams["seed"].asLong 34 | newParams.addProperty("seed", seed + idx) 35 | if (idx > 0) { 36 | newParams.addProperty("prompt", magicPromptResult[idx - 1]) 37 | } 38 | batch.add(newParams) 39 | } 40 | return FairQueueEntry( 41 | "Making Images", 42 | event.member!!.id, 43 | batch, 44 | getSdJsonDefaults(), 45 | sdHiddenParameters, 46 | getScriptForSize(batch[0].asJsonObject.get("size").asInt), 47 | hook, 48 | null, 49 | ChapterEntry.Companion.Type.Image, 50 | ChapterEntry.Companion.Visibility.Public, 51 | "jpg" 52 | ) 53 | } 54 | 55 | val embeds = checkForEmbeds(prompt, event.user.idLong) 56 | if (embeds.first.isNotEmpty()) { 57 | embedsCallback(jda, event, embeds, params) { _, params, hook -> 58 | runBlocking { 59 | val entry = createEntry(hook, params) 60 | //queueDispatcher.queue.addToQueue(entry) 61 | } 62 | } 63 | } 64 | else { 65 | val entry = createEntry(event.hook, params) 66 | //queueDispatcher.queue.addToQueue(entry) 67 | } 68 | } catch (e: Exception) { 69 | e.printStackTrace() 70 | event.sendException(e) 71 | } 72 | */ 73 | } 74 | } -------------------------------------------------------------------------------- /src/main/kotlin/commands/make/QuiltMaker.kt: -------------------------------------------------------------------------------- 1 | package commands.make 2 | 3 | import utils.resize 4 | import java.awt.image.BufferedImage 5 | import java.io.ByteArrayInputStream 6 | import java.io.ByteArrayOutputStream 7 | import javax.imageio.ImageIO 8 | import kotlin.math.max 9 | 10 | fun makeQuiltFromByteArrayList(images: List, formatName: String = "jpg"): ByteArray { 11 | val bufferedImages = images.map { bytes -> 12 | val inputStream = ByteArrayInputStream(bytes) 13 | val image = ImageIO.read(inputStream) 14 | image 15 | } 16 | val quilt = makeQuilt(bufferedImages) 17 | val baos = ByteArrayOutputStream() 18 | ImageIO.write(quilt, formatName, baos) 19 | return baos.toByteArray() 20 | } 21 | 22 | fun makeQuilt(images: List): BufferedImage { 23 | if (images.size == 1) { 24 | return images.first() 25 | } 26 | var largestWidth = 0 27 | var largestHeight = 0 28 | for(image in images) { 29 | largestWidth = max(image.width, largestWidth) 30 | largestHeight = max(image.height, largestHeight) 31 | } 32 | val resizedImages = images.map { 33 | it.resize(largestWidth, largestHeight) 34 | } 35 | val pic = if (resizedImages.size == 2) { 36 | BufferedImage(resizedImages[0].width * 2, resizedImages[0].height, BufferedImage.TYPE_INT_RGB) 37 | } else { 38 | BufferedImage(resizedImages[0].width * 2, resizedImages[0].height * 2, BufferedImage.TYPE_INT_RGB) 39 | } 40 | val g = pic.graphics 41 | for (y in 0 until 2) { 42 | for (x in 0 until 2) { 43 | val index = y * 2 + x 44 | if (index >= resizedImages.size) { 45 | break 46 | } 47 | val image = resizedImages[index] 48 | g.drawImage(image, x * image.width, y * image.height, null) 49 | } 50 | } 51 | g.dispose() 52 | return pic 53 | } 54 | -------------------------------------------------------------------------------- /src/main/kotlin/commands/make/Ratio.kt: -------------------------------------------------------------------------------- 1 | package commands.make 2 | 3 | import kotlin.math.floor 4 | import kotlin.math.max 5 | import kotlin.math.min 6 | 7 | data class Ratio( 8 | val w: Int = 1, 9 | val h: Int = 1 10 | ) { 11 | fun calculateSize(baseSize: Int): Pair { 12 | val r = max(this.w, this.h) / min(this.w, this.h).toDouble() 13 | val sW = floor(baseSize * r).toInt() 14 | val sH = floor(sW / r).toInt() 15 | 16 | return if (this.w < this.h) { 17 | sH to sW 18 | } else { 19 | sW to sH 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/commands/make/Utils.kt: -------------------------------------------------------------------------------- 1 | package commands.make 2 | 3 | fun getScriptForSize(size: Int): String { 4 | if (size >= 768) { 5 | return "stable_diffusion_768" 6 | } 7 | return "stable_diffusion_512" 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/commands/social/EditProfile.kt: -------------------------------------------------------------------------------- 1 | package commands.social 2 | 3 | import com.google.gson.JsonArray 4 | import database.chapterDao 5 | import database.userDao 6 | import dev.minn.jda.ktx.events.onCommand 7 | import dev.minn.jda.ktx.interactions.components.button 8 | import dev.minn.jda.ktx.messages.editMessage 9 | import dev.minn.jda.ktx.messages.reply_ 10 | import editMessageToIncludePaginator 11 | import gson 12 | import miniManual 13 | import net.dv8tion.jda.api.JDA 14 | import net.dv8tion.jda.api.entities.Message 15 | import net.dv8tion.jda.api.interactions.components.buttons.Button 16 | import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle 17 | import net.dv8tion.jda.api.requests.restaction.WebhookMessageEditAction 18 | import net.dv8tion.jda.api.utils.FileUpload 19 | import ui.makeSelectImageFromQuilt 20 | import utils.* 21 | import java.awt.image.BufferedImage 22 | import java.net.URL 23 | import javax.imageio.ImageIO 24 | 25 | fun setBackgroundCommand(jda: JDA) { 26 | jda.onCommand("edit_profile") { event -> 27 | try { 28 | val user = userDao.queryBuilder().selectColumns("id", "currentChapterId").where() 29 | .eq("discordUserID", event.user.id).queryForFirst() 30 | if (user == null) { 31 | event.reply_("User '${event.user.id}' not found! Did you make art yet? $miniManual") 32 | .setEphemeral(true).queue() 33 | return@onCommand 34 | } 35 | 36 | val usingChapter = 37 | chapterDao.queryBuilder().selectColumns().where() 38 | .eq("id", user.currentChapterId).and().eq("userID", user.id).queryForFirst() 39 | if (usingChapter == null) { 40 | event.reply_("Sorry, we couldn't find any chapters! $miniManual") 41 | .setEphemeral(true).queue() 42 | return@onCommand 43 | } 44 | 45 | event.deferReply(true).queue() 46 | val latestEntry = usingChapter.getLatestEntry() 47 | val image = ImageIO.read(URL(latestEntry.data)) 48 | val parameters = gson.fromJson(latestEntry.parameters, JsonArray::class.java) 49 | val quiltSelector = makeSelectImageFromQuilt( 50 | event.user, 51 | "Select image for background use!", 52 | image, 53 | parameters.size() 54 | ) { _, chosenImage -> 55 | val imageSlice = takeSlice(image, parameters.size(), chosenImage) 56 | var currentY = 0 57 | fun getSlicedBG(): BufferedImage { 58 | return imageSlice.getSubimage(0, currentY, imageSlice.width, profileCardHeight) 59 | } 60 | 61 | fun getCroppedCardBGMessage(): WebhookMessageEditAction { 62 | val card = makeProfileCard(event.user, getSlicedBG()) 63 | return event.hook.editMessage(content = "**Preview!**") 64 | .setFiles(FileUpload.fromData(bufferedImageToByteArray(card), "profile.png")) 65 | } 66 | 67 | fun getFontDropdown() { 68 | 69 | } 70 | 71 | fun getButtons(): Array