├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── LICENSE ├── README.md ├── biome.json ├── example.dev.vars ├── example.wrangler.toml ├── package-lock.json ├── package.json ├── renovate.json ├── src ├── commands.ts ├── esrgan.ts ├── index.ts ├── instructPix2Pix.ts ├── instructPix2PixHelpers.ts ├── interactions.ts ├── queue.ts ├── result.ts ├── types.ts └── util.ts └── tsconfig.json /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | name: ci 7 | jobs: 8 | lint: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-node@v4 13 | with: 14 | node-version: 20 15 | - run: npm install 16 | - run: npm run lint 17 | build: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions/setup-node@v4 22 | with: 23 | node-version: 20 24 | - run: npm install 25 | - run: npm run build 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .wrangler 3 | wrangler.toml 4 | .dev.vars 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Discord 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 | # Avatar Remix 2 | 3 | This project contains code for a Discord bot that will edit friends' profile pictures in fun and sometimes unexpected ways. 4 | 5 | ![avatar remix example](https://www.ianww.com/avatar-remix/catluvr2.png) 6 | 7 | ## Try it out 8 | 9 | [Replicate](https://replicate.com?utm_source=project&utm_campaign=avatar-remix-bot-repo) has provided a hosted version of this bot. You can [invite the hosted bot to your Discord server](https://discord.com/oauth2/authorize?client_id=1083422468263387227&scope=applications.commands+bot), or follow this README to set up and host your own custom version. 10 | 11 | ## How it works 12 | 13 | The main functionality of Avatar Remix is provided by [instruct-pix2pix](https://www.timothybrooks.com/instruct-pix2pix), a model based on Stable Diffusion fine-tuned on a large dataset of example prompts generated by GPT-3. 14 | 15 | Discord profile pictures are often pretty small, so if necessary it uses [Real-ESRGAN](https://github.com/xinntao/Real-ESRGAN) to upscale for better results. 16 | 17 | This app runs as a Cloudflare Worker and uses the [Replicate](https://replicate.com?utm_source=project&utm_campaign=avatar-remix-bot-repo) API to run the above models serverlessly. 18 | 19 | ![request flow](https://kroki.io/mermaid/svg/eNqVjzEOwjAMRfecIhfowlqJEzBxActNTCkNSbFjwfFJIgZQqRCDh299-_83Mi5nezj2xqgQA9NNSbLtur11Iak_BWSCe-KZuDerVTOi95ATlEul8oiER4zgMIQB3fzfL8nIGaYomdVlWKbHrgxc0tCb1fZXxFYGRV9IRUOuvSt4rR0bgK9ZzdYkuBRFr8TmU76VffG2ipumr0RPimmNGA==) 20 | 21 | ## Setup 22 | 23 | ### Install dependencies 24 | 25 | This bot runs on Node and its package manager, npm. If you don't already have node and npm set up, [following this guide](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). 26 | 27 | Install the bot's dependencies by running: 28 | 29 | ```bash 30 | npm install 31 | ``` 32 | 33 | ### Set up external accounts 34 | 35 | This bot uses [Cloudflare Workers](https://workers.cloudflare.com/) and [Replicate](https://replicate.com?utm_source=project&utm_campaign=avatar-remix-bot-repo). You'll need to: 36 | 37 | - Set up `wrangler` locally ([see Cloudflare docs](https://developers.cloudflare.com/workers/wrangler/install-and-update/)) 38 | - Get a [Replicate API token](https://replicate.com/account?utm_source=project&utm_campaign=avatar-remix-bot-repo) 39 | 40 | ### Create a Discord application 41 | 42 | Create a new application on the [Discord Developer portal](https://discord.com/developers/applications). Give it a name like `avatar-remix-bot`. 43 | 44 | Then, in the "Bot" settings tab of your application, click "Add Bot" (we don't actually need this bot user, but we use its token for convenience...) 45 | 46 | See the [Getting Started](https://discord.com/developers/docs/getting-started) guide on the Discord developer portal for more details. 47 | 48 | 49 | 50 | **NOTE: if you use the code in this project to build or operate an app or bot on Discord, you will need to comply with Discord’s [Developer Terms of Service and Policy](https://discord.com/developers/docs/policies-and-agreements/developer-terms-of-service). Use of this code does not guarantee compliance.** 51 | 52 | ### Envars and secrets 53 | 54 | Set up envars in `wrangler.toml`: 55 | 56 | - Rename `example.wrangler.toml` to `wrangler.toml` 57 | - Update `DISCORD_APPLICATION_ID` and `DISCORD_PUBLIC_KEY` with values from the application you created in the previous step. 58 | - Update `WORKER_BASE_URL` with the URL of your Cloudflare worker. By default, this URL takes the form `https://avatar-remix-bot..workers.dev`. 59 | 60 | Use wrangler to store secret tokens for your bot by running the following commands: 61 | 62 | - `wrangler secret put DISCORD_BOT_TOKEN` - obtain this token by clicking "View Token" on the Bot page 63 | - `wrangler secret put REPLICATE_API_TOKEN` - obtain this token from your [Replicate account settings](https://replicate.com/account?utm_source=project&utm_campaign=avatar-remix-bot-repo) 64 | 65 | If you want to run the app locally, rename `example.dev.vars` to `.dev.vars`, and update the variables with the appropriate secrets. **ENSURE `.dev.vars` is in your `.gitignore`, and do not share these secrets**. 66 | 67 | ### KV store and Queues 68 | 69 | This bot uses Cloudflare to host some serverless infrastructure. 70 | 71 | First, it uses [Workers KV](https://developers.cloudflare.com/workers/runtime-apis/kv/) in order to track Replicate jobs and match callbacks with the original jobs. Run the following command to create the KV namespace: 72 | 73 | ```sh 74 | wrangler kv:namespace create AVATAR_REMIX_FOLLOWUPS 75 | ``` 76 | 77 | Take the output and paste it into `wrangler.toml`, replacing the placeholder binding for `AVATAR_REMIX_FOLLOWUPS`. 78 | 79 | Second, it uses [Cloudflare Queues](https://developers.cloudflare.com/queues/platform/javascript-apis/) to stay within Replicate rate limits during periods of heavy load (this isn't necessary unless you think a lot of people will use your app): 80 | 81 | ```bash 82 | wrangler queues create avatar-remix-jobs 83 | ``` 84 | 85 | Note that Cloudflare Queues are in beta! You might have to enable them manually in the [Cloudflare dashboard](https://dash.cloudflare.com/) by going to Workers > Queues. 86 | 87 | ### Publish the Cloudflare Worker 88 | 89 | It's time to bring your worker online! Run the following: 90 | 91 | ```bash 92 | wrangler publish 93 | ``` 94 | 95 | If all went according to plan, you should see something like this: 96 | 97 | ```text 98 | Uploaded avatar-remix-bot (1.16 sec) 99 | Published avatar-remix-bot (0.67 sec) 100 | https://avatar-remix-bot..workers.dev 101 | Consumer for avatar-remix-jobs 102 | Current Deployment ID: f5d24e08-a211-4bae-a573-e513b5355910 103 | ``` 104 | 105 | If you go to `https://avatar-remix-bot..workers.dev`, you should see the message "greetings". That means your web application is online and ready to talk with Discord :) 106 | 107 | ### Register your Interactions Endpoint URL 108 | 109 | On the "General Information" tab of your Application's settings in the developer portal, scroll down and set the "Interactions Endpoint URL" to the URL of your Cloudflare Worker. 110 | 111 | The URL was outputted in the previous step, and typically it is of the form `https://avatar-remix-bot..workers.dev`. 112 | 113 | ![here's where the interaction endpoint url is set](https://user-images.githubusercontent.com/310310/223584868-ce3bc51d-fe8c-4255-a1d0-7c528d8c06f8.png) 114 | 115 | Once you've clicked Save Changes, Discord will test your endpoint and you will get a success message in the dev portal indicating that your changes have been saved. 116 | 117 | ### Register slash commands 118 | 119 | This bots uses a `/remix` slash command, and you must register the slash command before using it for the first time. Register it by hitting the register endpoint with a `POST` request: 120 | 121 | ```sh 122 | curl -X POST https://avatar-remix-bot..workers.dev/register 123 | ``` 124 | 125 | The response will just say "Registered commands". 126 | 127 | In the future, you'll have to do this any time you've made a change to `commands.ts`. 128 | 129 | ### Invite it to your server 130 | 131 | Your bot is ready to go! Invite it to your server by constructing an invite URL: 132 | 133 | ```text 134 | https://discord.com/oauth2/authorize?client_id=&scope=applications.commands+bot 135 | ``` 136 | 137 | Replace the `client_id` above with the `DISCORD_APPLICATION_ID` you recorded earlier. 138 | 139 | That's all! Use the `/remix` command and start remixing to your heart's content. 140 | 141 | ![partially typed remix command](https://user-images.githubusercontent.com/310310/223583186-269e875b-19b6-4fc5-8832-843bdd60cfec.png) 142 | 143 | REMINDER: Make sure your bot complies with the [Discord Developer Terms of Service and Policy](https://discord.com/developers/docs/policies-and-agreements/developer-terms-of-service). 144 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.6.4/schema.json", 3 | "files": { 4 | "include": ["src/**/*.ts", "examples/*.js"] 5 | }, 6 | "organizeImports": { 7 | "enabled": true 8 | }, 9 | "linter": { 10 | "enabled": true, 11 | "rules": { 12 | "recommended": true 13 | } 14 | }, 15 | "javascript": { 16 | "formatter": { 17 | "quoteStyle": "single" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example.dev.vars: -------------------------------------------------------------------------------- 1 | REPLICATE_API_TOKEN: 2 | DISCORD_BOT_TOKEN: > 3 | -------------------------------------------------------------------------------- /example.wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "avatar-remix-bot" 2 | main = "src/index.ts" 3 | compatibility_date = "2023-01-24" 4 | 5 | kv_namespaces = [ 6 | { binding = "AVATAR_REMIX_FOLLOWUPS", id = "", preview_id = "" } 7 | ] 8 | 9 | [[queues.producers]] 10 | queue = "avatar-remix-jobs" 11 | binding = "AVATAR_REMIX_JOBS" 12 | 13 | [[queues.consumers]] 14 | queue = "avatar-remix-jobs" 15 | max_batch_size = 6 # optional: defaults to 10 16 | max_batch_timeout = 1 # optional: defaults to 5 seconds 17 | 18 | [vars] 19 | DISCORD_PUBLIC_KEY = '6ebc1a23ce676ede92f44f0bf943af50b87a75a693cbe3c1d2ccbf353eaa220c' 20 | DISCORD_APPLICATION_ID = '1234567890123456789' 21 | 22 | WORKER_BASE_URL = 'https://avatar-remix-bot..workers.dev' 23 | 24 | REPLICATE_INSTRUCT_PIX2PIX_MODEL_VERSION = '30c1d0b916a6f8efce20493f5d61ee27491ab2a60437c13c588468b9810ec23f' 25 | REPLICATE_ESRGAN_MODEL_VERSION = '42fed1c4974146d4d2414e2be2c5277c7fcf05fcc3a73abf41610695738c1d7b' 26 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "avatar-remix", 3 | "version": "0.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "avatar-remix", 9 | "version": "0.0.0", 10 | "dependencies": { 11 | "discord-interactions": "^4.0.0", 12 | "itty-router": "^5.0.17", 13 | "upng-js": "^2.1.0" 14 | }, 15 | "devDependencies": { 16 | "@biomejs/biome": "^1.8.0", 17 | "@cloudflare/workers-types": "^4.20230115.0", 18 | "@types/upng-js": "^2.1.2", 19 | "discord-api-types": "^0.38.0", 20 | "typescript": "^5.0.0", 21 | "wrangler": "^3.59.0" 22 | } 23 | }, 24 | "node_modules/@biomejs/biome": { 25 | "version": "1.8.1", 26 | "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.8.1.tgz", 27 | "integrity": "sha512-fQXGfvq6DIXem12dGQCM2tNF+vsNHH1qs3C7WeOu75Pd0trduoTmoO7G4ntLJ2qDs5wuw981H+cxQhi1uHnAtA==", 28 | "dev": true, 29 | "hasInstallScript": true, 30 | "bin": { 31 | "biome": "bin/biome" 32 | }, 33 | "engines": { 34 | "node": ">=14.21.3" 35 | }, 36 | "funding": { 37 | "type": "opencollective", 38 | "url": "https://opencollective.com/biome" 39 | }, 40 | "optionalDependencies": { 41 | "@biomejs/cli-darwin-arm64": "1.8.1", 42 | "@biomejs/cli-darwin-x64": "1.8.1", 43 | "@biomejs/cli-linux-arm64": "1.8.1", 44 | "@biomejs/cli-linux-arm64-musl": "1.8.1", 45 | "@biomejs/cli-linux-x64": "1.8.1", 46 | "@biomejs/cli-linux-x64-musl": "1.8.1", 47 | "@biomejs/cli-win32-arm64": "1.8.1", 48 | "@biomejs/cli-win32-x64": "1.8.1" 49 | } 50 | }, 51 | "node_modules/@biomejs/cli-darwin-arm64": { 52 | "version": "1.8.1", 53 | "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.8.1.tgz", 54 | "integrity": "sha512-XLiB7Uu6GALIOBWzQ2aMD0ru4Ly5/qSeQF7kk3AabzJ/kwsEWSe33iVySBP/SS2qv25cgqNiLksjGcw2bHT3mw==", 55 | "cpu": [ 56 | "arm64" 57 | ], 58 | "dev": true, 59 | "optional": true, 60 | "os": [ 61 | "darwin" 62 | ], 63 | "engines": { 64 | "node": ">=14.21.3" 65 | } 66 | }, 67 | "node_modules/@biomejs/cli-darwin-x64": { 68 | "version": "1.8.1", 69 | "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.8.1.tgz", 70 | "integrity": "sha512-uMTSxVLMfqkBVqyc25hSn83jBbp+wtWjzM/pHFlKXt3htJuw7FErVGW0nmQ9Sxa9vJ7GcqoltLMl28VQRIMYzg==", 71 | "cpu": [ 72 | "x64" 73 | ], 74 | "dev": true, 75 | "optional": true, 76 | "os": [ 77 | "darwin" 78 | ], 79 | "engines": { 80 | "node": ">=14.21.3" 81 | } 82 | }, 83 | "node_modules/@biomejs/cli-linux-arm64": { 84 | "version": "1.8.1", 85 | "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.8.1.tgz", 86 | "integrity": "sha512-3SzZRuC/9Oi2P2IBNPsEj0KXxSXUEYRR2kfRF/Ve8QAfGgrt4qnwuWd6QQKKN5R+oYH691qjm+cXBKEcrP1v/Q==", 87 | "cpu": [ 88 | "arm64" 89 | ], 90 | "dev": true, 91 | "optional": true, 92 | "os": [ 93 | "linux" 94 | ], 95 | "engines": { 96 | "node": ">=14.21.3" 97 | } 98 | }, 99 | "node_modules/@biomejs/cli-linux-arm64-musl": { 100 | "version": "1.8.1", 101 | "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.8.1.tgz", 102 | "integrity": "sha512-UQ8Wc01J0wQL+5AYOc7qkJn20B4PZmQL1KrmDZh7ot0DvD6aX4+8mmfd/dG5b6Zjo/44QvCKcvkFGCMRYuhWZA==", 103 | "cpu": [ 104 | "arm64" 105 | ], 106 | "dev": true, 107 | "optional": true, 108 | "os": [ 109 | "linux" 110 | ], 111 | "engines": { 112 | "node": ">=14.21.3" 113 | } 114 | }, 115 | "node_modules/@biomejs/cli-linux-x64": { 116 | "version": "1.8.1", 117 | "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.8.1.tgz", 118 | "integrity": "sha512-AeBycVdNrTzsyYKEOtR2R0Ph0hCD0sCshcp2aOnfGP0hCZbtFg09D0SdKLbyzKntisY41HxKVrydYiaApp+2uw==", 119 | "cpu": [ 120 | "x64" 121 | ], 122 | "dev": true, 123 | "optional": true, 124 | "os": [ 125 | "linux" 126 | ], 127 | "engines": { 128 | "node": ">=14.21.3" 129 | } 130 | }, 131 | "node_modules/@biomejs/cli-linux-x64-musl": { 132 | "version": "1.8.1", 133 | "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.8.1.tgz", 134 | "integrity": "sha512-fYbP/kNu/rtZ4kKzWVocIdqZOtBSUEg9qUhZaao3dy3CRzafR6u6KDtBeSCnt47O+iLnks1eOR1TUxzr5+QuqA==", 135 | "cpu": [ 136 | "x64" 137 | ], 138 | "dev": true, 139 | "optional": true, 140 | "os": [ 141 | "linux" 142 | ], 143 | "engines": { 144 | "node": ">=14.21.3" 145 | } 146 | }, 147 | "node_modules/@biomejs/cli-win32-arm64": { 148 | "version": "1.8.1", 149 | "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.8.1.tgz", 150 | "integrity": "sha512-6tEd1H/iFKpgpE3OIB7oNgW5XkjiVMzMRPL8zYoZ036YfuJ5nMYm9eB9H/y81+8Z76vL48fiYzMPotJwukGPqQ==", 151 | "cpu": [ 152 | "arm64" 153 | ], 154 | "dev": true, 155 | "optional": true, 156 | "os": [ 157 | "win32" 158 | ], 159 | "engines": { 160 | "node": ">=14.21.3" 161 | } 162 | }, 163 | "node_modules/@biomejs/cli-win32-x64": { 164 | "version": "1.8.1", 165 | "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.8.1.tgz", 166 | "integrity": "sha512-g2H31jJzYmS4jkvl6TiyEjEX+Nv79a5km/xn+5DARTp5MBFzC9gwceusSSB2AkJKqZzY131AiACAWjKrVt5Ijw==", 167 | "cpu": [ 168 | "x64" 169 | ], 170 | "dev": true, 171 | "optional": true, 172 | "os": [ 173 | "win32" 174 | ], 175 | "engines": { 176 | "node": ">=14.21.3" 177 | } 178 | }, 179 | "node_modules/@cloudflare/kv-asset-handler": { 180 | "version": "0.3.2", 181 | "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.2.tgz", 182 | "integrity": "sha512-EeEjMobfuJrwoctj7FA1y1KEbM0+Q1xSjobIEyie9k4haVEBB7vkDvsasw1pM3rO39mL2akxIAzLMUAtrMHZhA==", 183 | "dev": true, 184 | "dependencies": { 185 | "mime": "^3.0.0" 186 | }, 187 | "engines": { 188 | "node": ">=16.13" 189 | } 190 | }, 191 | "node_modules/@cloudflare/workerd-darwin-64": { 192 | "version": "1.20240610.1", 193 | "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20240610.1.tgz", 194 | "integrity": "sha512-YanZ1iXgMGaUWlleB5cswSE6qbzyjQ8O7ENWZcPAcZZ6BfuL7q3CWi0t9iM1cv2qx92rRztsRTyjcfq099++XQ==", 195 | "cpu": [ 196 | "x64" 197 | ], 198 | "dev": true, 199 | "optional": true, 200 | "os": [ 201 | "darwin" 202 | ], 203 | "engines": { 204 | "node": ">=16" 205 | } 206 | }, 207 | "node_modules/@cloudflare/workerd-darwin-arm64": { 208 | "version": "1.20240610.1", 209 | "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20240610.1.tgz", 210 | "integrity": "sha512-bRe/y/LKjIgp3L2EHjc+CvoCzfHhf4aFTtOBkv2zW+VToNJ4KlXridndf7LvR9urfsFRRo9r4TXCssuKaU+ypQ==", 211 | "cpu": [ 212 | "arm64" 213 | ], 214 | "dev": true, 215 | "optional": true, 216 | "os": [ 217 | "darwin" 218 | ], 219 | "engines": { 220 | "node": ">=16" 221 | } 222 | }, 223 | "node_modules/@cloudflare/workerd-linux-64": { 224 | "version": "1.20240610.1", 225 | "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20240610.1.tgz", 226 | "integrity": "sha512-2zDcadR7+Gs9SjcMXmwsMji2Xs+yASGNA2cEHDuFc4NMUup+eL1mkzxc/QzvFjyBck98e92rBjMZt2dVscpGKg==", 227 | "cpu": [ 228 | "x64" 229 | ], 230 | "dev": true, 231 | "optional": true, 232 | "os": [ 233 | "linux" 234 | ], 235 | "engines": { 236 | "node": ">=16" 237 | } 238 | }, 239 | "node_modules/@cloudflare/workerd-linux-arm64": { 240 | "version": "1.20240610.1", 241 | "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20240610.1.tgz", 242 | "integrity": "sha512-7y41rPi5xmIYJN8CY+t3RHnjLL0xx/WYmaTd/j552k1qSr02eTE2o/TGyWZmGUC+lWnwdPQJla0mXbvdqgRdQg==", 243 | "cpu": [ 244 | "arm64" 245 | ], 246 | "dev": true, 247 | "optional": true, 248 | "os": [ 249 | "linux" 250 | ], 251 | "engines": { 252 | "node": ">=16" 253 | } 254 | }, 255 | "node_modules/@cloudflare/workerd-windows-64": { 256 | "version": "1.20240610.1", 257 | "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20240610.1.tgz", 258 | "integrity": "sha512-B0LyT3DB6rXHWNptnntYHPaoJIy0rXnGfeDBM3nEVV8JIsQrx8MEFn2F2jYioH1FkUVavsaqKO/zUosY3tZXVA==", 259 | "cpu": [ 260 | "x64" 261 | ], 262 | "dev": true, 263 | "optional": true, 264 | "os": [ 265 | "win32" 266 | ], 267 | "engines": { 268 | "node": ">=16" 269 | } 270 | }, 271 | "node_modules/@cloudflare/workers-types": { 272 | "version": "4.20240605.0", 273 | "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20240605.0.tgz", 274 | "integrity": "sha512-zJw4Q6CnkaQ5JZmHRkNiSs5GfiRgUIUL8BIHPQkd2XUHZkIBv9M9yc0LKEwMYGpCFC+oSOltet6c9RjP9uQ99g==", 275 | "dev": true 276 | }, 277 | "node_modules/@cspotcode/source-map-support": { 278 | "version": "0.8.1", 279 | "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", 280 | "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", 281 | "dev": true, 282 | "dependencies": { 283 | "@jridgewell/trace-mapping": "0.3.9" 284 | }, 285 | "engines": { 286 | "node": ">=12" 287 | } 288 | }, 289 | "node_modules/@esbuild-plugins/node-globals-polyfill": { 290 | "version": "0.2.3", 291 | "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz", 292 | "integrity": "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==", 293 | "dev": true, 294 | "peerDependencies": { 295 | "esbuild": "*" 296 | } 297 | }, 298 | "node_modules/@esbuild-plugins/node-modules-polyfill": { 299 | "version": "0.2.2", 300 | "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.2.2.tgz", 301 | "integrity": "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==", 302 | "dev": true, 303 | "dependencies": { 304 | "escape-string-regexp": "^4.0.0", 305 | "rollup-plugin-node-polyfills": "^0.2.1" 306 | }, 307 | "peerDependencies": { 308 | "esbuild": "*" 309 | } 310 | }, 311 | "node_modules/@esbuild/android-arm": { 312 | "version": "0.17.19", 313 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", 314 | "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", 315 | "cpu": [ 316 | "arm" 317 | ], 318 | "dev": true, 319 | "optional": true, 320 | "os": [ 321 | "android" 322 | ], 323 | "engines": { 324 | "node": ">=12" 325 | } 326 | }, 327 | "node_modules/@esbuild/android-arm64": { 328 | "version": "0.17.19", 329 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", 330 | "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", 331 | "cpu": [ 332 | "arm64" 333 | ], 334 | "dev": true, 335 | "optional": true, 336 | "os": [ 337 | "android" 338 | ], 339 | "engines": { 340 | "node": ">=12" 341 | } 342 | }, 343 | "node_modules/@esbuild/android-x64": { 344 | "version": "0.17.19", 345 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", 346 | "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", 347 | "cpu": [ 348 | "x64" 349 | ], 350 | "dev": true, 351 | "optional": true, 352 | "os": [ 353 | "android" 354 | ], 355 | "engines": { 356 | "node": ">=12" 357 | } 358 | }, 359 | "node_modules/@esbuild/darwin-arm64": { 360 | "version": "0.17.19", 361 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", 362 | "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", 363 | "cpu": [ 364 | "arm64" 365 | ], 366 | "dev": true, 367 | "optional": true, 368 | "os": [ 369 | "darwin" 370 | ], 371 | "engines": { 372 | "node": ">=12" 373 | } 374 | }, 375 | "node_modules/@esbuild/darwin-x64": { 376 | "version": "0.17.19", 377 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", 378 | "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", 379 | "cpu": [ 380 | "x64" 381 | ], 382 | "dev": true, 383 | "optional": true, 384 | "os": [ 385 | "darwin" 386 | ], 387 | "engines": { 388 | "node": ">=12" 389 | } 390 | }, 391 | "node_modules/@esbuild/freebsd-arm64": { 392 | "version": "0.17.19", 393 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", 394 | "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", 395 | "cpu": [ 396 | "arm64" 397 | ], 398 | "dev": true, 399 | "optional": true, 400 | "os": [ 401 | "freebsd" 402 | ], 403 | "engines": { 404 | "node": ">=12" 405 | } 406 | }, 407 | "node_modules/@esbuild/freebsd-x64": { 408 | "version": "0.17.19", 409 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", 410 | "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", 411 | "cpu": [ 412 | "x64" 413 | ], 414 | "dev": true, 415 | "optional": true, 416 | "os": [ 417 | "freebsd" 418 | ], 419 | "engines": { 420 | "node": ">=12" 421 | } 422 | }, 423 | "node_modules/@esbuild/linux-arm": { 424 | "version": "0.17.19", 425 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", 426 | "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", 427 | "cpu": [ 428 | "arm" 429 | ], 430 | "dev": true, 431 | "optional": true, 432 | "os": [ 433 | "linux" 434 | ], 435 | "engines": { 436 | "node": ">=12" 437 | } 438 | }, 439 | "node_modules/@esbuild/linux-arm64": { 440 | "version": "0.17.19", 441 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", 442 | "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", 443 | "cpu": [ 444 | "arm64" 445 | ], 446 | "dev": true, 447 | "optional": true, 448 | "os": [ 449 | "linux" 450 | ], 451 | "engines": { 452 | "node": ">=12" 453 | } 454 | }, 455 | "node_modules/@esbuild/linux-ia32": { 456 | "version": "0.17.19", 457 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", 458 | "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", 459 | "cpu": [ 460 | "ia32" 461 | ], 462 | "dev": true, 463 | "optional": true, 464 | "os": [ 465 | "linux" 466 | ], 467 | "engines": { 468 | "node": ">=12" 469 | } 470 | }, 471 | "node_modules/@esbuild/linux-loong64": { 472 | "version": "0.17.19", 473 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", 474 | "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", 475 | "cpu": [ 476 | "loong64" 477 | ], 478 | "dev": true, 479 | "optional": true, 480 | "os": [ 481 | "linux" 482 | ], 483 | "engines": { 484 | "node": ">=12" 485 | } 486 | }, 487 | "node_modules/@esbuild/linux-mips64el": { 488 | "version": "0.17.19", 489 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", 490 | "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", 491 | "cpu": [ 492 | "mips64el" 493 | ], 494 | "dev": true, 495 | "optional": true, 496 | "os": [ 497 | "linux" 498 | ], 499 | "engines": { 500 | "node": ">=12" 501 | } 502 | }, 503 | "node_modules/@esbuild/linux-ppc64": { 504 | "version": "0.17.19", 505 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", 506 | "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", 507 | "cpu": [ 508 | "ppc64" 509 | ], 510 | "dev": true, 511 | "optional": true, 512 | "os": [ 513 | "linux" 514 | ], 515 | "engines": { 516 | "node": ">=12" 517 | } 518 | }, 519 | "node_modules/@esbuild/linux-riscv64": { 520 | "version": "0.17.19", 521 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", 522 | "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", 523 | "cpu": [ 524 | "riscv64" 525 | ], 526 | "dev": true, 527 | "optional": true, 528 | "os": [ 529 | "linux" 530 | ], 531 | "engines": { 532 | "node": ">=12" 533 | } 534 | }, 535 | "node_modules/@esbuild/linux-s390x": { 536 | "version": "0.17.19", 537 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", 538 | "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", 539 | "cpu": [ 540 | "s390x" 541 | ], 542 | "dev": true, 543 | "optional": true, 544 | "os": [ 545 | "linux" 546 | ], 547 | "engines": { 548 | "node": ">=12" 549 | } 550 | }, 551 | "node_modules/@esbuild/linux-x64": { 552 | "version": "0.17.19", 553 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", 554 | "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", 555 | "cpu": [ 556 | "x64" 557 | ], 558 | "dev": true, 559 | "optional": true, 560 | "os": [ 561 | "linux" 562 | ], 563 | "engines": { 564 | "node": ">=12" 565 | } 566 | }, 567 | "node_modules/@esbuild/netbsd-x64": { 568 | "version": "0.17.19", 569 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", 570 | "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", 571 | "cpu": [ 572 | "x64" 573 | ], 574 | "dev": true, 575 | "optional": true, 576 | "os": [ 577 | "netbsd" 578 | ], 579 | "engines": { 580 | "node": ">=12" 581 | } 582 | }, 583 | "node_modules/@esbuild/openbsd-x64": { 584 | "version": "0.17.19", 585 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", 586 | "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", 587 | "cpu": [ 588 | "x64" 589 | ], 590 | "dev": true, 591 | "optional": true, 592 | "os": [ 593 | "openbsd" 594 | ], 595 | "engines": { 596 | "node": ">=12" 597 | } 598 | }, 599 | "node_modules/@esbuild/sunos-x64": { 600 | "version": "0.17.19", 601 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", 602 | "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", 603 | "cpu": [ 604 | "x64" 605 | ], 606 | "dev": true, 607 | "optional": true, 608 | "os": [ 609 | "sunos" 610 | ], 611 | "engines": { 612 | "node": ">=12" 613 | } 614 | }, 615 | "node_modules/@esbuild/win32-arm64": { 616 | "version": "0.17.19", 617 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", 618 | "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", 619 | "cpu": [ 620 | "arm64" 621 | ], 622 | "dev": true, 623 | "optional": true, 624 | "os": [ 625 | "win32" 626 | ], 627 | "engines": { 628 | "node": ">=12" 629 | } 630 | }, 631 | "node_modules/@esbuild/win32-ia32": { 632 | "version": "0.17.19", 633 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", 634 | "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", 635 | "cpu": [ 636 | "ia32" 637 | ], 638 | "dev": true, 639 | "optional": true, 640 | "os": [ 641 | "win32" 642 | ], 643 | "engines": { 644 | "node": ">=12" 645 | } 646 | }, 647 | "node_modules/@esbuild/win32-x64": { 648 | "version": "0.17.19", 649 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", 650 | "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", 651 | "cpu": [ 652 | "x64" 653 | ], 654 | "dev": true, 655 | "optional": true, 656 | "os": [ 657 | "win32" 658 | ], 659 | "engines": { 660 | "node": ">=12" 661 | } 662 | }, 663 | "node_modules/@fastify/busboy": { 664 | "version": "2.1.1", 665 | "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", 666 | "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", 667 | "dev": true, 668 | "engines": { 669 | "node": ">=14" 670 | } 671 | }, 672 | "node_modules/@jridgewell/resolve-uri": { 673 | "version": "3.1.2", 674 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 675 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 676 | "dev": true, 677 | "engines": { 678 | "node": ">=6.0.0" 679 | } 680 | }, 681 | "node_modules/@jridgewell/sourcemap-codec": { 682 | "version": "1.4.15", 683 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", 684 | "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", 685 | "dev": true 686 | }, 687 | "node_modules/@jridgewell/trace-mapping": { 688 | "version": "0.3.9", 689 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", 690 | "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", 691 | "dev": true, 692 | "dependencies": { 693 | "@jridgewell/resolve-uri": "^3.0.3", 694 | "@jridgewell/sourcemap-codec": "^1.4.10" 695 | } 696 | }, 697 | "node_modules/@types/node": { 698 | "version": "20.14.2", 699 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", 700 | "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", 701 | "dev": true, 702 | "dependencies": { 703 | "undici-types": "~5.26.4" 704 | } 705 | }, 706 | "node_modules/@types/node-forge": { 707 | "version": "1.3.11", 708 | "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", 709 | "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", 710 | "dev": true, 711 | "dependencies": { 712 | "@types/node": "*" 713 | } 714 | }, 715 | "node_modules/@types/upng-js": { 716 | "version": "2.1.5", 717 | "resolved": "https://registry.npmjs.org/@types/upng-js/-/upng-js-2.1.5.tgz", 718 | "integrity": "sha512-CzXg1lcCcWzrmYmke9BLbBPzb2DpdC1bXuXf0BtK3Bygvsozslei8S1bheDI1QUfZzZpMeQI5fywfnMj4CxocQ==", 719 | "dev": true 720 | }, 721 | "node_modules/acorn": { 722 | "version": "8.12.0", 723 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", 724 | "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", 725 | "dev": true, 726 | "bin": { 727 | "acorn": "bin/acorn" 728 | }, 729 | "engines": { 730 | "node": ">=0.4.0" 731 | } 732 | }, 733 | "node_modules/acorn-walk": { 734 | "version": "8.3.3", 735 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", 736 | "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", 737 | "dev": true, 738 | "dependencies": { 739 | "acorn": "^8.11.0" 740 | }, 741 | "engines": { 742 | "node": ">=0.4.0" 743 | } 744 | }, 745 | "node_modules/anymatch": { 746 | "version": "3.1.3", 747 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", 748 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 749 | "dev": true, 750 | "dependencies": { 751 | "normalize-path": "^3.0.0", 752 | "picomatch": "^2.0.4" 753 | }, 754 | "engines": { 755 | "node": ">= 8" 756 | } 757 | }, 758 | "node_modules/as-table": { 759 | "version": "1.0.55", 760 | "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", 761 | "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", 762 | "dev": true, 763 | "dependencies": { 764 | "printable-characters": "^1.0.42" 765 | } 766 | }, 767 | "node_modules/binary-extensions": { 768 | "version": "2.3.0", 769 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", 770 | "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", 771 | "dev": true, 772 | "engines": { 773 | "node": ">=8" 774 | }, 775 | "funding": { 776 | "url": "https://github.com/sponsors/sindresorhus" 777 | } 778 | }, 779 | "node_modules/blake3-wasm": { 780 | "version": "2.1.5", 781 | "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", 782 | "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", 783 | "dev": true 784 | }, 785 | "node_modules/braces": { 786 | "version": "3.0.3", 787 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", 788 | "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", 789 | "dev": true, 790 | "dependencies": { 791 | "fill-range": "^7.1.1" 792 | }, 793 | "engines": { 794 | "node": ">=8" 795 | } 796 | }, 797 | "node_modules/capnp-ts": { 798 | "version": "0.7.0", 799 | "resolved": "https://registry.npmjs.org/capnp-ts/-/capnp-ts-0.7.0.tgz", 800 | "integrity": "sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==", 801 | "dev": true, 802 | "dependencies": { 803 | "debug": "^4.3.1", 804 | "tslib": "^2.2.0" 805 | } 806 | }, 807 | "node_modules/chokidar": { 808 | "version": "3.6.0", 809 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", 810 | "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", 811 | "dev": true, 812 | "dependencies": { 813 | "anymatch": "~3.1.2", 814 | "braces": "~3.0.2", 815 | "glob-parent": "~5.1.2", 816 | "is-binary-path": "~2.1.0", 817 | "is-glob": "~4.0.1", 818 | "normalize-path": "~3.0.0", 819 | "readdirp": "~3.6.0" 820 | }, 821 | "engines": { 822 | "node": ">= 8.10.0" 823 | }, 824 | "funding": { 825 | "url": "https://paulmillr.com/funding/" 826 | }, 827 | "optionalDependencies": { 828 | "fsevents": "~2.3.2" 829 | } 830 | }, 831 | "node_modules/consola": { 832 | "version": "3.2.3", 833 | "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", 834 | "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", 835 | "dev": true, 836 | "engines": { 837 | "node": "^14.18.0 || >=16.10.0" 838 | } 839 | }, 840 | "node_modules/cookie": { 841 | "version": "0.5.0", 842 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", 843 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", 844 | "dev": true, 845 | "engines": { 846 | "node": ">= 0.6" 847 | } 848 | }, 849 | "node_modules/data-uri-to-buffer": { 850 | "version": "2.0.2", 851 | "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", 852 | "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", 853 | "dev": true 854 | }, 855 | "node_modules/debug": { 856 | "version": "4.3.5", 857 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", 858 | "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", 859 | "dev": true, 860 | "dependencies": { 861 | "ms": "2.1.2" 862 | }, 863 | "engines": { 864 | "node": ">=6.0" 865 | }, 866 | "peerDependenciesMeta": { 867 | "supports-color": { 868 | "optional": true 869 | } 870 | } 871 | }, 872 | "node_modules/defu": { 873 | "version": "6.1.4", 874 | "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", 875 | "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", 876 | "dev": true 877 | }, 878 | "node_modules/discord-api-types": { 879 | "version": "0.38.10", 880 | "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.10.tgz", 881 | "integrity": "sha512-6F02RttmlDoTpFEeOlF1Z9lmNDUFum4ewBPFFjkD6mIlnd+NJ6oze/dllPdp8dpNvFuLHyEfVy+UPZ1s+IWmmA==", 882 | "dev": true, 883 | "license": "MIT", 884 | "workspaces": [ 885 | "scripts/actions/documentation" 886 | ] 887 | }, 888 | "node_modules/discord-interactions": { 889 | "version": "4.0.0", 890 | "resolved": "https://registry.npmjs.org/discord-interactions/-/discord-interactions-4.0.0.tgz", 891 | "integrity": "sha512-27HSJ379W9E4T6Or+efQRGnekJ6g8kYokSFtK5HKc/3ObC+83O+vxdg2trStD/2XPIHVPMNVIoZAeK9s/EL8Yg==", 892 | "engines": { 893 | "node": ">=18.4.0" 894 | } 895 | }, 896 | "node_modules/esbuild": { 897 | "version": "0.17.19", 898 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", 899 | "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", 900 | "dev": true, 901 | "hasInstallScript": true, 902 | "bin": { 903 | "esbuild": "bin/esbuild" 904 | }, 905 | "engines": { 906 | "node": ">=12" 907 | }, 908 | "optionalDependencies": { 909 | "@esbuild/android-arm": "0.17.19", 910 | "@esbuild/android-arm64": "0.17.19", 911 | "@esbuild/android-x64": "0.17.19", 912 | "@esbuild/darwin-arm64": "0.17.19", 913 | "@esbuild/darwin-x64": "0.17.19", 914 | "@esbuild/freebsd-arm64": "0.17.19", 915 | "@esbuild/freebsd-x64": "0.17.19", 916 | "@esbuild/linux-arm": "0.17.19", 917 | "@esbuild/linux-arm64": "0.17.19", 918 | "@esbuild/linux-ia32": "0.17.19", 919 | "@esbuild/linux-loong64": "0.17.19", 920 | "@esbuild/linux-mips64el": "0.17.19", 921 | "@esbuild/linux-ppc64": "0.17.19", 922 | "@esbuild/linux-riscv64": "0.17.19", 923 | "@esbuild/linux-s390x": "0.17.19", 924 | "@esbuild/linux-x64": "0.17.19", 925 | "@esbuild/netbsd-x64": "0.17.19", 926 | "@esbuild/openbsd-x64": "0.17.19", 927 | "@esbuild/sunos-x64": "0.17.19", 928 | "@esbuild/win32-arm64": "0.17.19", 929 | "@esbuild/win32-ia32": "0.17.19", 930 | "@esbuild/win32-x64": "0.17.19" 931 | } 932 | }, 933 | "node_modules/escape-string-regexp": { 934 | "version": "4.0.0", 935 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 936 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 937 | "dev": true, 938 | "engines": { 939 | "node": ">=10" 940 | }, 941 | "funding": { 942 | "url": "https://github.com/sponsors/sindresorhus" 943 | } 944 | }, 945 | "node_modules/estree-walker": { 946 | "version": "0.6.1", 947 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", 948 | "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", 949 | "dev": true 950 | }, 951 | "node_modules/exit-hook": { 952 | "version": "2.2.1", 953 | "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", 954 | "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", 955 | "dev": true, 956 | "engines": { 957 | "node": ">=6" 958 | }, 959 | "funding": { 960 | "url": "https://github.com/sponsors/sindresorhus" 961 | } 962 | }, 963 | "node_modules/fill-range": { 964 | "version": "7.1.1", 965 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", 966 | "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", 967 | "dev": true, 968 | "dependencies": { 969 | "to-regex-range": "^5.0.1" 970 | }, 971 | "engines": { 972 | "node": ">=8" 973 | } 974 | }, 975 | "node_modules/fsevents": { 976 | "version": "2.3.3", 977 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 978 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 979 | "dev": true, 980 | "hasInstallScript": true, 981 | "optional": true, 982 | "os": [ 983 | "darwin" 984 | ], 985 | "engines": { 986 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 987 | } 988 | }, 989 | "node_modules/function-bind": { 990 | "version": "1.1.2", 991 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 992 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 993 | "dev": true, 994 | "funding": { 995 | "url": "https://github.com/sponsors/ljharb" 996 | } 997 | }, 998 | "node_modules/get-source": { 999 | "version": "2.0.12", 1000 | "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", 1001 | "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", 1002 | "dev": true, 1003 | "dependencies": { 1004 | "data-uri-to-buffer": "^2.0.0", 1005 | "source-map": "^0.6.1" 1006 | } 1007 | }, 1008 | "node_modules/glob-parent": { 1009 | "version": "5.1.2", 1010 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 1011 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 1012 | "dev": true, 1013 | "dependencies": { 1014 | "is-glob": "^4.0.1" 1015 | }, 1016 | "engines": { 1017 | "node": ">= 6" 1018 | } 1019 | }, 1020 | "node_modules/glob-to-regexp": { 1021 | "version": "0.4.1", 1022 | "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", 1023 | "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", 1024 | "dev": true 1025 | }, 1026 | "node_modules/hasown": { 1027 | "version": "2.0.2", 1028 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 1029 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 1030 | "dev": true, 1031 | "dependencies": { 1032 | "function-bind": "^1.1.2" 1033 | }, 1034 | "engines": { 1035 | "node": ">= 0.4" 1036 | } 1037 | }, 1038 | "node_modules/is-binary-path": { 1039 | "version": "2.1.0", 1040 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 1041 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 1042 | "dev": true, 1043 | "dependencies": { 1044 | "binary-extensions": "^2.0.0" 1045 | }, 1046 | "engines": { 1047 | "node": ">=8" 1048 | } 1049 | }, 1050 | "node_modules/is-core-module": { 1051 | "version": "2.13.1", 1052 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", 1053 | "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", 1054 | "dev": true, 1055 | "dependencies": { 1056 | "hasown": "^2.0.0" 1057 | }, 1058 | "funding": { 1059 | "url": "https://github.com/sponsors/ljharb" 1060 | } 1061 | }, 1062 | "node_modules/is-extglob": { 1063 | "version": "2.1.1", 1064 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 1065 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 1066 | "dev": true, 1067 | "engines": { 1068 | "node": ">=0.10.0" 1069 | } 1070 | }, 1071 | "node_modules/is-glob": { 1072 | "version": "4.0.3", 1073 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 1074 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 1075 | "dev": true, 1076 | "dependencies": { 1077 | "is-extglob": "^2.1.1" 1078 | }, 1079 | "engines": { 1080 | "node": ">=0.10.0" 1081 | } 1082 | }, 1083 | "node_modules/is-number": { 1084 | "version": "7.0.0", 1085 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 1086 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 1087 | "dev": true, 1088 | "engines": { 1089 | "node": ">=0.12.0" 1090 | } 1091 | }, 1092 | "node_modules/itty-router": { 1093 | "version": "5.0.17", 1094 | "resolved": "https://registry.npmjs.org/itty-router/-/itty-router-5.0.17.tgz", 1095 | "integrity": "sha512-ZHnPI0OOyTTLuNp2FdciejYaK4Wl3ZV3O0yEm8njOGggh/k/ek3BL7X2I5YsCOfc5vLhIJgj3Z4pUtLs6k9Ucg==" 1096 | }, 1097 | "node_modules/magic-string": { 1098 | "version": "0.25.9", 1099 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", 1100 | "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", 1101 | "dev": true, 1102 | "dependencies": { 1103 | "sourcemap-codec": "^1.4.8" 1104 | } 1105 | }, 1106 | "node_modules/mime": { 1107 | "version": "3.0.0", 1108 | "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", 1109 | "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", 1110 | "dev": true, 1111 | "bin": { 1112 | "mime": "cli.js" 1113 | }, 1114 | "engines": { 1115 | "node": ">=10.0.0" 1116 | } 1117 | }, 1118 | "node_modules/miniflare": { 1119 | "version": "3.20240610.0", 1120 | "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20240610.0.tgz", 1121 | "integrity": "sha512-J6aXmkII5gcq+kC4TurxKiR4rC++apPST/K8P/YjqoQQgrJ+NRPacBhf6iVh8R3ujnXYXaq+Ae+gm+LM0XHK/w==", 1122 | "dev": true, 1123 | "dependencies": { 1124 | "@cspotcode/source-map-support": "0.8.1", 1125 | "acorn": "^8.8.0", 1126 | "acorn-walk": "^8.2.0", 1127 | "capnp-ts": "^0.7.0", 1128 | "exit-hook": "^2.2.1", 1129 | "glob-to-regexp": "^0.4.1", 1130 | "stoppable": "^1.1.0", 1131 | "undici": "^5.28.2", 1132 | "workerd": "1.20240610.1", 1133 | "ws": "^8.11.0", 1134 | "youch": "^3.2.2", 1135 | "zod": "^3.20.6" 1136 | }, 1137 | "bin": { 1138 | "miniflare": "bootstrap.js" 1139 | }, 1140 | "engines": { 1141 | "node": ">=16.13" 1142 | } 1143 | }, 1144 | "node_modules/ms": { 1145 | "version": "2.1.2", 1146 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 1147 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 1148 | "dev": true 1149 | }, 1150 | "node_modules/mustache": { 1151 | "version": "4.2.0", 1152 | "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", 1153 | "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", 1154 | "dev": true, 1155 | "bin": { 1156 | "mustache": "bin/mustache" 1157 | } 1158 | }, 1159 | "node_modules/nanoid": { 1160 | "version": "3.3.7", 1161 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", 1162 | "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", 1163 | "dev": true, 1164 | "funding": [ 1165 | { 1166 | "type": "github", 1167 | "url": "https://github.com/sponsors/ai" 1168 | } 1169 | ], 1170 | "bin": { 1171 | "nanoid": "bin/nanoid.cjs" 1172 | }, 1173 | "engines": { 1174 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 1175 | } 1176 | }, 1177 | "node_modules/node-fetch-native": { 1178 | "version": "1.6.4", 1179 | "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz", 1180 | "integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==", 1181 | "dev": true 1182 | }, 1183 | "node_modules/node-forge": { 1184 | "version": "1.3.1", 1185 | "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", 1186 | "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", 1187 | "dev": true, 1188 | "engines": { 1189 | "node": ">= 6.13.0" 1190 | } 1191 | }, 1192 | "node_modules/normalize-path": { 1193 | "version": "3.0.0", 1194 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 1195 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 1196 | "dev": true, 1197 | "engines": { 1198 | "node": ">=0.10.0" 1199 | } 1200 | }, 1201 | "node_modules/pako": { 1202 | "version": "1.0.11", 1203 | "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", 1204 | "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" 1205 | }, 1206 | "node_modules/path-parse": { 1207 | "version": "1.0.7", 1208 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 1209 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", 1210 | "dev": true 1211 | }, 1212 | "node_modules/path-to-regexp": { 1213 | "version": "6.2.2", 1214 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", 1215 | "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", 1216 | "dev": true 1217 | }, 1218 | "node_modules/pathe": { 1219 | "version": "1.1.2", 1220 | "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", 1221 | "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", 1222 | "dev": true 1223 | }, 1224 | "node_modules/picomatch": { 1225 | "version": "2.3.1", 1226 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 1227 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 1228 | "dev": true, 1229 | "engines": { 1230 | "node": ">=8.6" 1231 | }, 1232 | "funding": { 1233 | "url": "https://github.com/sponsors/jonschlinkert" 1234 | } 1235 | }, 1236 | "node_modules/printable-characters": { 1237 | "version": "1.0.42", 1238 | "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", 1239 | "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", 1240 | "dev": true 1241 | }, 1242 | "node_modules/readdirp": { 1243 | "version": "3.6.0", 1244 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 1245 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 1246 | "dev": true, 1247 | "dependencies": { 1248 | "picomatch": "^2.2.1" 1249 | }, 1250 | "engines": { 1251 | "node": ">=8.10.0" 1252 | } 1253 | }, 1254 | "node_modules/resolve": { 1255 | "version": "1.22.8", 1256 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", 1257 | "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", 1258 | "dev": true, 1259 | "dependencies": { 1260 | "is-core-module": "^2.13.0", 1261 | "path-parse": "^1.0.7", 1262 | "supports-preserve-symlinks-flag": "^1.0.0" 1263 | }, 1264 | "bin": { 1265 | "resolve": "bin/resolve" 1266 | }, 1267 | "funding": { 1268 | "url": "https://github.com/sponsors/ljharb" 1269 | } 1270 | }, 1271 | "node_modules/resolve.exports": { 1272 | "version": "2.0.2", 1273 | "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", 1274 | "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", 1275 | "dev": true, 1276 | "engines": { 1277 | "node": ">=10" 1278 | } 1279 | }, 1280 | "node_modules/rollup-plugin-inject": { 1281 | "version": "3.0.2", 1282 | "resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz", 1283 | "integrity": "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==", 1284 | "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.", 1285 | "dev": true, 1286 | "dependencies": { 1287 | "estree-walker": "^0.6.1", 1288 | "magic-string": "^0.25.3", 1289 | "rollup-pluginutils": "^2.8.1" 1290 | } 1291 | }, 1292 | "node_modules/rollup-plugin-node-polyfills": { 1293 | "version": "0.2.1", 1294 | "resolved": "https://registry.npmjs.org/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz", 1295 | "integrity": "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==", 1296 | "dev": true, 1297 | "dependencies": { 1298 | "rollup-plugin-inject": "^3.0.0" 1299 | } 1300 | }, 1301 | "node_modules/rollup-pluginutils": { 1302 | "version": "2.8.2", 1303 | "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", 1304 | "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", 1305 | "dev": true, 1306 | "dependencies": { 1307 | "estree-walker": "^0.6.1" 1308 | } 1309 | }, 1310 | "node_modules/selfsigned": { 1311 | "version": "2.4.1", 1312 | "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", 1313 | "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", 1314 | "dev": true, 1315 | "dependencies": { 1316 | "@types/node-forge": "^1.3.0", 1317 | "node-forge": "^1" 1318 | }, 1319 | "engines": { 1320 | "node": ">=10" 1321 | } 1322 | }, 1323 | "node_modules/source-map": { 1324 | "version": "0.6.1", 1325 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 1326 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 1327 | "dev": true, 1328 | "engines": { 1329 | "node": ">=0.10.0" 1330 | } 1331 | }, 1332 | "node_modules/sourcemap-codec": { 1333 | "version": "1.4.8", 1334 | "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", 1335 | "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", 1336 | "deprecated": "Please use @jridgewell/sourcemap-codec instead", 1337 | "dev": true 1338 | }, 1339 | "node_modules/stacktracey": { 1340 | "version": "2.1.8", 1341 | "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", 1342 | "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", 1343 | "dev": true, 1344 | "dependencies": { 1345 | "as-table": "^1.0.36", 1346 | "get-source": "^2.0.12" 1347 | } 1348 | }, 1349 | "node_modules/stoppable": { 1350 | "version": "1.1.0", 1351 | "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", 1352 | "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", 1353 | "dev": true, 1354 | "engines": { 1355 | "node": ">=4", 1356 | "npm": ">=6" 1357 | } 1358 | }, 1359 | "node_modules/supports-preserve-symlinks-flag": { 1360 | "version": "1.0.0", 1361 | "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", 1362 | "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", 1363 | "dev": true, 1364 | "engines": { 1365 | "node": ">= 0.4" 1366 | }, 1367 | "funding": { 1368 | "url": "https://github.com/sponsors/ljharb" 1369 | } 1370 | }, 1371 | "node_modules/to-regex-range": { 1372 | "version": "5.0.1", 1373 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1374 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1375 | "dev": true, 1376 | "dependencies": { 1377 | "is-number": "^7.0.0" 1378 | }, 1379 | "engines": { 1380 | "node": ">=8.0" 1381 | } 1382 | }, 1383 | "node_modules/tslib": { 1384 | "version": "2.6.3", 1385 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", 1386 | "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", 1387 | "dev": true 1388 | }, 1389 | "node_modules/typescript": { 1390 | "version": "5.4.5", 1391 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", 1392 | "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", 1393 | "dev": true, 1394 | "bin": { 1395 | "tsc": "bin/tsc", 1396 | "tsserver": "bin/tsserver" 1397 | }, 1398 | "engines": { 1399 | "node": ">=14.17" 1400 | } 1401 | }, 1402 | "node_modules/ufo": { 1403 | "version": "1.5.3", 1404 | "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", 1405 | "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==", 1406 | "dev": true 1407 | }, 1408 | "node_modules/undici": { 1409 | "version": "5.28.4", 1410 | "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", 1411 | "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", 1412 | "dev": true, 1413 | "dependencies": { 1414 | "@fastify/busboy": "^2.0.0" 1415 | }, 1416 | "engines": { 1417 | "node": ">=14.0" 1418 | } 1419 | }, 1420 | "node_modules/undici-types": { 1421 | "version": "5.26.5", 1422 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 1423 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", 1424 | "dev": true 1425 | }, 1426 | "node_modules/unenv": { 1427 | "name": "unenv-nightly", 1428 | "version": "1.10.0-1717606461.a117952", 1429 | "resolved": "https://registry.npmjs.org/unenv-nightly/-/unenv-nightly-1.10.0-1717606461.a117952.tgz", 1430 | "integrity": "sha512-u3TfBX02WzbHTpaEfWEKwDijDSFAHcgXkayUZ+MVDrjhLFvgAJzFGTSTmwlEhwWi2exyRQey23ah9wELMM6etg==", 1431 | "dev": true, 1432 | "dependencies": { 1433 | "consola": "^3.2.3", 1434 | "defu": "^6.1.4", 1435 | "mime": "^3.0.0", 1436 | "node-fetch-native": "^1.6.4", 1437 | "pathe": "^1.1.2", 1438 | "ufo": "^1.5.3" 1439 | } 1440 | }, 1441 | "node_modules/upng-js": { 1442 | "version": "2.1.0", 1443 | "resolved": "https://registry.npmjs.org/upng-js/-/upng-js-2.1.0.tgz", 1444 | "integrity": "sha512-d3xzZzpMP64YkjP5pr8gNyvBt7dLk/uGI67EctzDuVp4lCZyVMo0aJO6l/VDlgbInJYDY6cnClLoBp29eKWI6g==", 1445 | "dependencies": { 1446 | "pako": "^1.0.5" 1447 | } 1448 | }, 1449 | "node_modules/workerd": { 1450 | "version": "1.20240610.1", 1451 | "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20240610.1.tgz", 1452 | "integrity": "sha512-Rtut5GrsODQMh6YU43b9WZ980Wd05Ov1/ds88pT/SoetmXFBvkBzdRfiHiATv+azmGX8KveE0i/Eqzk/yI01ug==", 1453 | "dev": true, 1454 | "hasInstallScript": true, 1455 | "bin": { 1456 | "workerd": "bin/workerd" 1457 | }, 1458 | "engines": { 1459 | "node": ">=16" 1460 | }, 1461 | "optionalDependencies": { 1462 | "@cloudflare/workerd-darwin-64": "1.20240610.1", 1463 | "@cloudflare/workerd-darwin-arm64": "1.20240610.1", 1464 | "@cloudflare/workerd-linux-64": "1.20240610.1", 1465 | "@cloudflare/workerd-linux-arm64": "1.20240610.1", 1466 | "@cloudflare/workerd-windows-64": "1.20240610.1" 1467 | } 1468 | }, 1469 | "node_modules/wrangler": { 1470 | "version": "3.60.3", 1471 | "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.60.3.tgz", 1472 | "integrity": "sha512-a6zn/KFnYaYp3nxJR/aP0TeaBvJDkrrfI89KoxUtx28H7zpya/5/VLu3CxQ3PRspEojJGF0s6f3/pddRy3F+BQ==", 1473 | "dev": true, 1474 | "dependencies": { 1475 | "@cloudflare/kv-asset-handler": "0.3.2", 1476 | "@esbuild-plugins/node-globals-polyfill": "^0.2.3", 1477 | "@esbuild-plugins/node-modules-polyfill": "^0.2.2", 1478 | "blake3-wasm": "^2.1.5", 1479 | "chokidar": "^3.5.3", 1480 | "esbuild": "0.17.19", 1481 | "miniflare": "3.20240610.0", 1482 | "nanoid": "^3.3.3", 1483 | "path-to-regexp": "^6.2.0", 1484 | "resolve": "^1.22.8", 1485 | "resolve.exports": "^2.0.2", 1486 | "selfsigned": "^2.0.1", 1487 | "source-map": "0.6.1", 1488 | "unenv": "npm:unenv-nightly@1.10.0-1717606461.a117952", 1489 | "xxhash-wasm": "^1.0.1" 1490 | }, 1491 | "bin": { 1492 | "wrangler": "bin/wrangler.js", 1493 | "wrangler2": "bin/wrangler.js" 1494 | }, 1495 | "engines": { 1496 | "node": ">=16.17.0" 1497 | }, 1498 | "optionalDependencies": { 1499 | "fsevents": "~2.3.2" 1500 | }, 1501 | "peerDependencies": { 1502 | "@cloudflare/workers-types": "^4.20240605.0" 1503 | }, 1504 | "peerDependenciesMeta": { 1505 | "@cloudflare/workers-types": { 1506 | "optional": true 1507 | } 1508 | } 1509 | }, 1510 | "node_modules/ws": { 1511 | "version": "8.17.0", 1512 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", 1513 | "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", 1514 | "dev": true, 1515 | "engines": { 1516 | "node": ">=10.0.0" 1517 | }, 1518 | "peerDependencies": { 1519 | "bufferutil": "^4.0.1", 1520 | "utf-8-validate": ">=5.0.2" 1521 | }, 1522 | "peerDependenciesMeta": { 1523 | "bufferutil": { 1524 | "optional": true 1525 | }, 1526 | "utf-8-validate": { 1527 | "optional": true 1528 | } 1529 | } 1530 | }, 1531 | "node_modules/xxhash-wasm": { 1532 | "version": "1.0.2", 1533 | "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.0.2.tgz", 1534 | "integrity": "sha512-ibF0Or+FivM9lNrg+HGJfVX8WJqgo+kCLDc4vx6xMeTce7Aj+DLttKbxxRR/gNLSAelRc1omAPlJ77N/Jem07A==", 1535 | "dev": true 1536 | }, 1537 | "node_modules/youch": { 1538 | "version": "3.3.3", 1539 | "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.3.tgz", 1540 | "integrity": "sha512-qSFXUk3UZBLfggAW3dJKg0BMblG5biqSF8M34E06o5CSsZtH92u9Hqmj2RzGiHDi64fhe83+4tENFP2DB6t6ZA==", 1541 | "dev": true, 1542 | "dependencies": { 1543 | "cookie": "^0.5.0", 1544 | "mustache": "^4.2.0", 1545 | "stacktracey": "^2.1.8" 1546 | } 1547 | }, 1548 | "node_modules/zod": { 1549 | "version": "3.23.8", 1550 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", 1551 | "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", 1552 | "dev": true, 1553 | "funding": { 1554 | "url": "https://github.com/sponsors/colinhacks" 1555 | } 1556 | } 1557 | } 1558 | } 1559 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "avatar-remix", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "wrangler dev --port 3000", 7 | "deploy": "wrangler publish", 8 | "build": "tsc", 9 | "fix": "biome check --write .", 10 | "lint": "biome check ." 11 | }, 12 | "dependencies": { 13 | "discord-interactions": "^4.0.0", 14 | "itty-router": "^5.0.17", 15 | "upng-js": "^2.1.0" 16 | }, 17 | "devDependencies": { 18 | "@biomejs/biome": "^1.8.0", 19 | "@cloudflare/workers-types": "^4.20230115.0", 20 | "@types/upng-js": "^2.1.2", 21 | "discord-api-types": "^0.38.0", 22 | "typescript": "^5.0.0", 23 | "wrangler": "^3.59.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended", 5 | ":disableDependencyDashboard", 6 | ":preserveSemverRanges" 7 | ], 8 | "ignorePaths": [ 9 | "**/node_modules/**" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/commands.ts: -------------------------------------------------------------------------------- 1 | // Slash command definition 2 | import { 3 | ApplicationCommandOptionType, 4 | ApplicationIntegrationType, 5 | InteractionContextType, 6 | } from 'discord-api-types/v10'; 7 | 8 | export const REMIX_COMMAND = { 9 | name: 'remix', 10 | description: "Remix someone's profile picture", 11 | integration_types: [ 12 | ApplicationIntegrationType.GuildInstall, 13 | ApplicationIntegrationType.UserInstall, 14 | ], 15 | contexts: [ 16 | InteractionContextType.Guild, 17 | InteractionContextType.BotDM, 18 | InteractionContextType.PrivateChannel, 19 | ], 20 | options: [ 21 | { 22 | name: 'user', 23 | description: 'The user whose profile picture you want to remix', 24 | type: ApplicationCommandOptionType.User, 25 | required: true, 26 | }, 27 | { 28 | name: 'instruction', 29 | description: 'What change do you want to make to the profile picture?', 30 | type: ApplicationCommandOptionType.String, 31 | required: true, 32 | }, 33 | { 34 | name: 'strength', 35 | description: 'The strength of the prompt. Defaults to 7', 36 | type: ApplicationCommandOptionType.Number, 37 | required: false, 38 | min_value: 1.0, 39 | max_value: 20.0, 40 | }, 41 | { 42 | name: 'seed', 43 | description: 'Defaults to a random number', 44 | type: ApplicationCommandOptionType.Integer, 45 | required: false, 46 | min_value: 0, 47 | max_value: 1e9, 48 | }, 49 | { 50 | name: 'debug-url', 51 | description: 'Debug tool. Not for you!', 52 | type: ApplicationCommandOptionType.String, 53 | required: false, 54 | }, 55 | ], 56 | }; 57 | -------------------------------------------------------------------------------- /src/esrgan.ts: -------------------------------------------------------------------------------- 1 | import { startInstructPix2Pix } from './instructPix2Pix'; 2 | import { interactionFollowup } from './util'; 3 | 4 | import type { IRequest } from 'itty-router'; 5 | import type { Env, Job } from './types'; 6 | 7 | export async function startEsrgan(job: Job, env: Env, scaleFactor: number) { 8 | const replicatePayload = { 9 | version: env.REPLICATE_ESRGAN_MODEL_VERSION, 10 | webhook_completed: `${env.WORKER_BASE_URL}/replicate/callback/esrgan`, 11 | input: { 12 | image: job.url, 13 | scale: scaleFactor, 14 | face_enhance: true, 15 | }, 16 | }; 17 | console.log({ esrganPayload: replicatePayload }); 18 | const response = await fetch('https://api.replicate.com/v1/predictions', { 19 | method: 'POST', 20 | headers: { 21 | 'Content-Type': 'application/json', 22 | Authorization: `Token ${env.REPLICATE_API_TOKEN}`, 23 | }, 24 | body: JSON.stringify(replicatePayload), 25 | }); 26 | 27 | if (response.ok) { 28 | const body = (await response.json()) as { id: string }; 29 | await env.AVATAR_REMIX_FOLLOWUPS.put( 30 | `replicateId:${body.id}`, 31 | JSON.stringify(job), 32 | { 33 | expirationTtl: 60 * 60 * 24 * 14, // 2 weeks (used for remix remixes) 34 | }, 35 | ); 36 | } else { 37 | let error = `${response.status} ${response.statusText}`; 38 | try { 39 | const errText = await response.text(); 40 | if (errText) { 41 | error += `: ${errText}`; 42 | } 43 | } catch {} 44 | console.error('Failed to start esrgan job', { ...job, error }); 45 | 46 | const discordUrl = `https://discord.com/api/v10/webhooks/${env.DISCORD_APPLICATION_ID}/${job.interactionToken}`; 47 | await fetch(discordUrl, { 48 | method: 'POST', 49 | headers: { 50 | 'Content-Type': 'application/json', 51 | }, 52 | body: JSON.stringify({ 53 | content: 54 | 'Failed to remix the avatar (resizing). Please try again later.', 55 | flags: 1 << 6, // ephemeral 56 | }), 57 | }); 58 | } 59 | } 60 | 61 | interface EsrganOutput { 62 | id: string; 63 | status: string; 64 | output?: string; 65 | } 66 | 67 | export async function esrganHandler( 68 | request: IRequest, 69 | env: Env, 70 | context: ExecutionContext, 71 | ) { 72 | const body = (await request.json()) as EsrganOutput; 73 | const replicateId = body.id; 74 | 75 | const jobString = await env.AVATAR_REMIX_FOLLOWUPS.get( 76 | `replicateId:${replicateId}`, 77 | ); 78 | if (!jobString) { 79 | return new Response('No job found', { status: 404 }); 80 | } 81 | const job = JSON.parse(jobString) as Job; 82 | 83 | console.log('Got replicate esrgan callback', { body, job: jobString }); 84 | 85 | if (body.status !== 'succeeded' || !body.output) { 86 | await interactionFollowup( 87 | 'Failed to remix the avatar (resizing). Please try again later.', 88 | job.interactionToken, 89 | env, 90 | ); 91 | return new Response('FAIL'); 92 | } 93 | 94 | job.url = body.output; 95 | await startInstructPix2Pix(job, env); 96 | return new Response('OK'); 97 | } 98 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { AutoRouter } from 'itty-router'; 2 | 3 | import { esrganHandler } from './esrgan'; 4 | import { instructPix2PixHandler } from './instructPix2Pix'; 5 | import queueHandler from './queue'; 6 | 7 | import { interactionsHandler, registerCommandsHandler } from './interactions'; 8 | 9 | const router = AutoRouter(); 10 | 11 | router.get('/', (request, env) => { 12 | return new Response('greetings'); 13 | }); 14 | 15 | router.post('/register', registerCommandsHandler); 16 | router.post('/', interactionsHandler); 17 | router.post('/replicate/callback/pix2pix', instructPix2PixHandler); 18 | router.post('/replicate/callback/esrgan', esrganHandler); 19 | 20 | router.all('*', () => new Response('Not Found.', { status: 404 })); 21 | 22 | export default { 23 | fetch: router.fetch, 24 | queue: queueHandler, 25 | }; 26 | -------------------------------------------------------------------------------- /src/instructPix2Pix.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NEGATIVE_PROMPT, 3 | USE_NEGATIVE_PROMPT, 4 | getImageGuidanceFromPromptGuidance, 5 | getValidStrengthValue, 6 | } from './instructPix2PixHelpers'; 7 | import { sendResult } from './result'; 8 | import { interactionFollowup } from './util'; 9 | 10 | import type { IRequest } from 'itty-router'; 11 | import type { Env, Job } from './types'; 12 | 13 | export async function instructPix2PixHandler(request: IRequest, env: Env) { 14 | const body = (await request.json()) as { 15 | id: string; 16 | status: string; 17 | output?: string[]; 18 | }; 19 | const replicateId = body.id; 20 | 21 | const jobString = await env.AVATAR_REMIX_FOLLOWUPS.get( 22 | `replicateId:${replicateId}`, 23 | ); 24 | if (!jobString) { 25 | console.error( 26 | `Received instructpix2pix callback for unknown replicateId ${replicateId}`, 27 | ); 28 | return new Response('No job found', { status: 404 }); 29 | } 30 | const job = JSON.parse(jobString) as Job; 31 | 32 | const { groupId } = request.query; 33 | console.log(`Got pix2pix callback with groupId ${groupId}`); 34 | if (!groupId) { 35 | await interactionFollowup( 36 | 'Could not get group ID, please try again.', 37 | job.interactionToken, 38 | env, 39 | ); 40 | return new Response('FAIL'); 41 | } 42 | console.log('Got replicate pix2pix callback', { body, job: jobString }); 43 | 44 | if (body.status !== 'succeeded' || !body.output || body.output.length < 1) { 45 | await interactionFollowup( 46 | 'Failed to remix the avatar. Please try again later.', 47 | job.interactionToken, 48 | env, 49 | ); 50 | return new Response('FAIL'); 51 | } 52 | 53 | const imageUrls = body.output; 54 | job.outputUrl = imageUrls[0]; 55 | await sendResult(body.id, job.outputUrl, job, env); 56 | 57 | return new Response('OK'); 58 | } 59 | 60 | export async function startInstructPix2Pix(job: Job, env: Env) { 61 | const promptGuidance = getValidStrengthValue(job.strength); 62 | const replicatePayload = { 63 | version: env.REPLICATE_INSTRUCT_PIX2PIX_MODEL_VERSION, 64 | webhook_completed: `${env.WORKER_BASE_URL}/replicate/callback/pix2pix?groupId=test`, 65 | input: { 66 | image: job.url, 67 | prompt: job.prompt, 68 | negative_prompt: USE_NEGATIVE_PROMPT 69 | ? job.negativePrompt || NEGATIVE_PROMPT 70 | : undefined, 71 | num_inference_steps: 50, 72 | guidance_scale: promptGuidance, 73 | image_guidance_scale: getImageGuidanceFromPromptGuidance(promptGuidance), 74 | schedule: 'K_EULER_ANCESTRAL', 75 | seed: job.seed, 76 | num_outputs: 1, 77 | }, 78 | }; 79 | 80 | const response = await fetch('https://api.replicate.com/v1/predictions', { 81 | method: 'POST', 82 | headers: { 83 | 'Content-Type': 'application/json', 84 | Authorization: `Token ${env.REPLICATE_API_TOKEN}`, 85 | }, 86 | body: JSON.stringify(replicatePayload), 87 | }); 88 | 89 | if (response.ok) { 90 | const body = (await response.json()) as { id: string }; 91 | console.log(`Saving replicateId ${body.id}`); 92 | await env.AVATAR_REMIX_FOLLOWUPS.put( 93 | `replicateId:${body.id}`, 94 | JSON.stringify(job), 95 | { 96 | expirationTtl: 60 * 60 * 24 * 14, // 2 weeks (used for remix remixes) 97 | }, 98 | ); 99 | } else { 100 | let error = `${response.status} ${response.statusText}`; 101 | try { 102 | const errText = await response.text(); 103 | if (errText) { 104 | error += `: ${errText}`; 105 | } 106 | } catch {} 107 | console.error('Failed to start job', { ...job, error }); 108 | 109 | await interactionFollowup( 110 | 'Failed to remix the avatar. Please try again later.', 111 | job.interactionToken, 112 | env, 113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/instructPix2PixHelpers.ts: -------------------------------------------------------------------------------- 1 | export const NEGATIVE_PROMPT = 2 | 'duplication, duplicates, mutilation, deformed, mutilated, mutation, twisted body, disfigured, bad anatomy, out of frame, extra fingers, mutated hands, poorly drawn hands, extra limbs, malformed limbs, missing arms, extra arms, missing legs, extra legs, mutated hands, extra hands, fused fingers, missing fingers, extra fingers, rolling eyes, crossed eyes, weird eyes, smudged face, blurred face, poorly drawn face, mutation, mutilation, cloned face, grainy, blurred, blurry, writing, calligraphy, signature, text, watermark, bad art'; 3 | 4 | export const USE_NEGATIVE_PROMPT = true; 5 | 6 | export function getValidStrengthValue( 7 | val: string | number | null | undefined, 8 | ): number { 9 | if (!val) { 10 | return 7; 11 | } 12 | 13 | let num = val; 14 | if (typeof val !== 'number') { 15 | num = Number.parseInt(val, 10); 16 | } 17 | if (Number.isNaN(num as number)) { 18 | return 7; 19 | } 20 | return Math.max(1, Math.min(20, num as number)); 21 | } 22 | 23 | export function getImageGuidanceFromPromptGuidance( 24 | promptGuidance: number, 25 | ): number { 26 | // There is much that can be done here, but let's just choose a reasonable image guidance for most use cases by default. 27 | return 1.5; 28 | } 29 | -------------------------------------------------------------------------------- /src/interactions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InteractionResponseType, 3 | InteractionType, 4 | verifyKey, 5 | } from 'discord-interactions'; 6 | import { REMIX_COMMAND } from './commands'; 7 | import { getValidStrengthValue } from './instructPix2PixHelpers'; 8 | import { JsonResponse, interactionStatus } from './util'; 9 | 10 | import type { IRequest } from 'itty-router'; 11 | import type { Env, Job } from './types'; 12 | 13 | function getOptionValue( 14 | options: { name: string; value: string }[], 15 | name: string, 16 | ) { 17 | const option = options.find((o) => o.name === name); 18 | return option?.value as T; 19 | } 20 | 21 | export async function registerCommandsHandler(request: IRequest, env: Env) { 22 | const token = env.DISCORD_BOT_TOKEN; 23 | const applicationId = env.DISCORD_APPLICATION_ID; 24 | 25 | const url = `https://discord.com/api/v10/applications/${applicationId}/commands`; 26 | const response = await fetch(url, { 27 | method: 'PUT', 28 | headers: { 29 | 'Content-Type': 'application/json', 30 | Authorization: `Bot ${token}`, 31 | }, 32 | body: JSON.stringify([REMIX_COMMAND]), 33 | }); 34 | 35 | if (!response.ok) { 36 | return new Response('Failed to register commands\n', { 37 | status: 500, 38 | }); 39 | } 40 | return new Response('Registered commands\n'); 41 | } 42 | 43 | export async function interactionsHandler( 44 | request: IRequest, 45 | env: Env, 46 | context: ExecutionContext, 47 | ) { 48 | // Using the incoming headers, verify this request actually came from discord. 49 | const signature = request.headers.get('x-signature-ed25519'); 50 | const timestamp = request.headers.get('x-signature-timestamp'); 51 | if (!signature || !timestamp) { 52 | return new Response('Bad request signature.', { status: 401 }); 53 | } 54 | 55 | const body = await request.clone().arrayBuffer(); 56 | const isValidRequest = await verifyKey( 57 | body, 58 | signature, 59 | timestamp, 60 | env.DISCORD_PUBLIC_KEY, 61 | ); 62 | if (!isValidRequest) { 63 | console.error('Invalid Request'); 64 | return new Response('Bad request signature.', { status: 401 }); 65 | } 66 | 67 | // biome-ignore lint/suspicious/noExplicitAny: 68 | const message = await request.json(); 69 | if (message.type === InteractionType.PING) { 70 | // The `PING` message is used during the initial webhook handshake, and is 71 | // required to configure the webhook in the developer portal. 72 | return new JsonResponse({ 73 | type: InteractionResponseType.PONG, 74 | }); 75 | } 76 | 77 | if (message.type === InteractionType.MESSAGE_COMPONENT) { 78 | const customId = message.data.custom_id; 79 | const splits = customId.split(':'); 80 | const command = splits[0]; 81 | const payload = splits[1]; 82 | 83 | const jobString = await env.AVATAR_REMIX_FOLLOWUPS.get( 84 | `replicateId:${payload}`, 85 | ); 86 | if (!jobString) { 87 | return interactionStatus( 88 | "It's been a while so I deleted the info I had on this job. Please generate a new image.", 89 | ); 90 | } 91 | const job = JSON.parse(jobString) as Job; 92 | 93 | if (command === 'retry') { 94 | await env.AVATAR_REMIX_JOBS.send({ 95 | type: job.type, 96 | url: job.url, 97 | prompt: job.prompt, 98 | targetUserId: job.targetUserId, 99 | remixRemix: job.remixRemix, 100 | strength: job.strength, 101 | 102 | requesterUserId: message.member?.user?.id || message.user?.id, 103 | seed: Math.floor(Math.random() * 1e9), 104 | interactionToken: message.token, 105 | }); 106 | return interactionStatus('one moment please...'); 107 | } 108 | 109 | const isRemix = command === 'remix'; 110 | 111 | const components = [ 112 | { 113 | type: 1, 114 | components: [ 115 | { 116 | type: 4, 117 | style: 1, 118 | custom_id: 'instructions', 119 | label: 'Instructions', 120 | placeholder: 'make it spooky', 121 | value: isRemix ? undefined : job.prompt, 122 | min_length: 1, 123 | max_length: 500, 124 | required: true, 125 | }, 126 | ], 127 | }, 128 | ]; 129 | if (!isRemix) { 130 | // Add strength 131 | components.push({ 132 | type: 1, 133 | components: [ 134 | { 135 | type: 4, 136 | style: 1, 137 | custom_id: 'strength', 138 | label: 'Strength (1-20)', 139 | placeholder: '7', 140 | value: String(getValidStrengthValue(job.strength)), 141 | min_length: 1, 142 | max_length: 2, 143 | required: false, 144 | }, 145 | ], 146 | }); 147 | } 148 | 149 | // Show a modal 150 | return new JsonResponse({ 151 | type: InteractionResponseType.MODAL, 152 | data: { 153 | title: isRemix ? 'Remix your remix' : 'Remix', 154 | content: 'Tell me what you want to do.', 155 | custom_id: customId, 156 | components, 157 | }, 158 | }); 159 | } 160 | 161 | if (message.type === InteractionType.MODAL_SUBMIT) { 162 | const splits = message.data.custom_id.split(':'); 163 | const command = splits[0]; 164 | const payload = decodeURIComponent(splits[1]); 165 | if (!command || !payload) { 166 | return interactionStatus('Invalid command'); 167 | } 168 | 169 | const jobString = await env.AVATAR_REMIX_FOLLOWUPS.get( 170 | `replicateId:${payload}`, 171 | ); 172 | if (!jobString) { 173 | return interactionStatus( 174 | "It's been a while so I deleted the info I had on this job. Please generate a new image.", 175 | ); 176 | } 177 | const previousJob = JSON.parse(jobString) as Job; 178 | if (!previousJob.outputUrl) { 179 | return interactionStatus( 180 | 'Could not find job output. Try again in a moment...', 181 | ); 182 | } 183 | 184 | switch (command) { 185 | case 'remix': 186 | await env.AVATAR_REMIX_JOBS.send({ 187 | type: 'REMIX_REMIX', 188 | url: previousJob.outputUrl, 189 | prompt: message.data.components[0].components[0].value, 190 | requesterUserId: message.member?.user?.id || message.user?.id, 191 | remixRemix: true, 192 | 193 | interactionToken: message.token, 194 | 195 | seed: Math.floor(Math.random() * 1e9), 196 | strength: previousJob.strength || 7, 197 | }); 198 | return interactionStatus('one moment please...'); 199 | case 'edit': 200 | await env.AVATAR_REMIX_JOBS.send({ 201 | type: 'REMIX_EDIT', 202 | url: previousJob.url, 203 | prompt: message.data.components[0].components[0].value, 204 | requesterUserId: message.member?.user?.id || message.user?.id, 205 | targetUserId: previousJob.targetUserId, 206 | remixRemix: previousJob.remixRemix, 207 | 208 | interactionToken: message.token, 209 | 210 | strength: getValidStrengthValue( 211 | message.data.components[1].components[0].value, 212 | ), 213 | seed: Math.floor(Math.random() * 1e9), 214 | }); 215 | return interactionStatus('one moment please...'); 216 | default: 217 | return interactionStatus('Unknown command'); 218 | } 219 | } 220 | 221 | if (message.type === InteractionType.APPLICATION_COMMAND) { 222 | let requesterUserId: string; 223 | let prompt: string; 224 | switch (message.data.name.toLowerCase()) { 225 | case REMIX_COMMAND.name: { 226 | const userId = getOptionValue(message.data.options, 'user'); 227 | const resolvedMemberAvatar = 228 | message.data.resolved.members?.[userId]?.avatar; 229 | const resolvedUserAvatar = 230 | message.data.resolved.users?.[userId]?.avatar; 231 | if (!resolvedUserAvatar) { 232 | return interactionStatus( 233 | `I couldn't find a user with the ID \`${userId}\``, 234 | ); 235 | } 236 | 237 | const avatarUrl = resolvedMemberAvatar 238 | ? `https://cdn.discordapp.com/guilds/${message.guild_id}/users/${userId}/avatars/${resolvedMemberAvatar}.png?size=512` 239 | : `https://cdn.discordapp.com/avatars/${userId}/${resolvedUserAvatar}.png?size=512`; 240 | 241 | console.log({ avatarUrl }); 242 | 243 | requesterUserId = message.member?.user?.id || message.user?.id; 244 | const debugUrl = getOptionValue(message.data.options, 'debug-url'); 245 | 246 | // Enqueue job to process the avatar remix. 247 | prompt = getOptionValue(message.data.options, 'instruction') 248 | .trim() 249 | .slice(0, 500); 250 | await env.AVATAR_REMIX_JOBS.send({ 251 | type: 'REMIX', 252 | url: debugUrl || avatarUrl, 253 | prompt, 254 | strength: getOptionValue(message.data.options, 'strength') || 7, 255 | seed: 256 | getOptionValue(message.data.options, 'seed') ?? 257 | Math.floor(Math.random() * 1e9), 258 | interactionToken: message.token, 259 | targetUserId: userId, 260 | requesterUserId, 261 | }); 262 | return new JsonResponse({ 263 | type: InteractionResponseType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE, 264 | }); 265 | } 266 | default: 267 | console.error(`Unknown command ${message.data.name}`); 268 | return interactionStatus('Unknown command'); 269 | } 270 | } 271 | 272 | console.error('Unknown message type', message.type); 273 | return new JsonResponse({ error: 'Unknown Type' }, { status: 400 }); 274 | } 275 | -------------------------------------------------------------------------------- /src/queue.ts: -------------------------------------------------------------------------------- 1 | import UPNG from 'upng-js'; 2 | 3 | import { startEsrgan } from './esrgan'; 4 | import { startInstructPix2Pix } from './instructPix2Pix'; 5 | import { interactionFollowup } from './util'; 6 | 7 | import type { Env, Job } from './types'; 8 | 9 | const MIN_IMAGE_SIZE_PX = 504; 10 | 11 | export default async function queue( 12 | batch: MessageBatch, 13 | env: Env, 14 | context: ExecutionContext, 15 | ): Promise { 16 | await Promise.all( 17 | batch.messages.map(async (message) => { 18 | const job = message.body; 19 | 20 | const imageResp = await fetch(job.url); 21 | const data = await imageResp.blob(); 22 | let image: UPNG.Image; 23 | try { 24 | image = UPNG.decode(await data.arrayBuffer()); 25 | } catch (err) { 26 | await interactionFollowup( 27 | 'Supports PNG only for now. Please enter a valid PNG file.', 28 | job.interactionToken, 29 | env, 30 | ); 31 | return; 32 | } 33 | 34 | if (Math.max(image.height, image.width) < MIN_IMAGE_SIZE_PX) { 35 | // Resize images that are too small 36 | return startEsrgan( 37 | job, 38 | env, 39 | Math.ceil((MIN_IMAGE_SIZE_PX - 1) / image.width), 40 | ); 41 | } 42 | 43 | return startInstructPix2Pix(job, env); 44 | }), 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/result.ts: -------------------------------------------------------------------------------- 1 | import type { Env, Job } from './types'; 2 | 3 | export async function sendResult( 4 | replicateId: string, 5 | imageUrl: string, 6 | job: Job, 7 | env: Env, 8 | ) { 9 | // Record followup with output URL, so that it can be remixed. 10 | await env.AVATAR_REMIX_FOLLOWUPS.put( 11 | `replicateId:${replicateId}`, 12 | JSON.stringify(job), 13 | { 14 | expirationTtl: 60 * 60 * 24 * 14, // 2 weeks (used for remix remixes) 15 | }, 16 | ); 17 | 18 | const imageResp = await fetch(imageUrl); 19 | const image = await imageResp.blob(); 20 | 21 | // Send the image to discord 22 | const hasTargetBeenPinged = await env.AVATAR_REMIX_FOLLOWUPS.get( 23 | `pinged:${job.targetUserId}`, 24 | ); 25 | const pings = [job.requesterUserId]; 26 | if (job.targetUserId && !hasTargetBeenPinged) { 27 | pings.push(job.targetUserId); 28 | await env.AVATAR_REMIX_FOLLOWUPS.put(`pinged:${job.targetUserId}`, '1', { 29 | expirationTtl: 60 * 60 * 15, // ping once per 15 min 30 | }); 31 | } 32 | 33 | let content: string; 34 | if (job.type.startsWith('EDIT')) { 35 | content = `<@${job.requesterUserId}> edited an image! \n\n> **${job.prompt}** \n\n _(remix strength: ${job.strength}, seed: ${job.seed}, original: <${job.url}>)_`; 36 | } else if (job.remixRemix) { 37 | content = `<@${job.requesterUserId}> remixed a remix\n\n> **${job.prompt}** \n\n _(remix strength: ${job.strength}, seed: ${job.seed})_`; 38 | } else { 39 | content = `<@${job.requesterUserId}> remixed <@${job.targetUserId}>'s profile picture!\n\n> **${job.prompt}** \n\n _(remix strength: ${job.strength}, seed: ${job.seed})_`; 40 | } 41 | 42 | const attachments = [ 43 | { 44 | id: 0, 45 | description: `Remix: ${job.prompt}`, 46 | filename: 'remix.png', 47 | }, 48 | ]; 49 | const msgJson = { 50 | content, 51 | components: [ 52 | { 53 | type: 1, 54 | components: [ 55 | { 56 | type: 2, 57 | style: 1, 58 | label: 'remix the remix', 59 | emoji: { 60 | name: '♻️', 61 | }, 62 | custom_id: `remix:${replicateId}`, 63 | }, 64 | { 65 | type: 2, 66 | style: 1, 67 | label: 'try it again', 68 | emoji: { 69 | name: '🎲', 70 | }, 71 | custom_id: `retry:${replicateId}`, 72 | }, 73 | { 74 | type: 2, 75 | style: 2, 76 | label: 'edit instructions', 77 | emoji: { 78 | name: '✏️', 79 | }, 80 | custom_id: `edit:${replicateId}`, 81 | }, 82 | ], 83 | }, 84 | ], 85 | attachments, 86 | allowed_mentions: { 87 | users: pings, 88 | }, 89 | }; 90 | 91 | // https://stackoverflow.com/a/35206069 92 | const formData = new FormData(); 93 | formData.append('payload_json', JSON.stringify(msgJson)); 94 | formData.append('files[0]', image as Blob, 'remix.png'); 95 | 96 | const discordUrl = `https://discord.com/api/v10/webhooks/${env.DISCORD_APPLICATION_ID}/${job.interactionToken}`; 97 | if (!job.remixRemix) { 98 | await fetch(`${discordUrl}/messages/@original`, { 99 | method: 'DELETE', 100 | }); 101 | } 102 | const discordResponse = await fetch(discordUrl, { 103 | method: 'POST', 104 | body: formData, 105 | }); 106 | if (!discordResponse.ok) { 107 | console.error('Failed to send followup to discord', discordResponse.status); 108 | const json = await discordResponse.json(); 109 | console.error({ response: json, msgJson: JSON.stringify(msgJson) }); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface Job { 2 | type: string; 3 | url: string; 4 | outputUrl?: string; 5 | prompt: string; 6 | interactionToken: string; 7 | targetUserId?: string; 8 | requesterUserId: string; 9 | strength?: number; 10 | seed?: number; 11 | remixRemix?: boolean; 12 | negativePrompt?: boolean; 13 | } 14 | 15 | export interface Env { 16 | AVATAR_REMIX_JOBS: Queue; 17 | AVATAR_REMIX_FOLLOWUPS: KVNamespace; 18 | 19 | DISCORD_PUBLIC_KEY: string; 20 | DISCORD_BOT_TOKEN: string; 21 | DISCORD_APPLICATION_ID: string; 22 | 23 | REPLICATE_API_TOKEN: string; 24 | REPLICATE_INSTRUCT_PIX2PIX_MODEL_VERSION: string; 25 | REPLICATE_ESRGAN_MODEL_VERSION: string; 26 | 27 | WORKER_BASE_URL: string; 28 | } 29 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import { InteractionResponseType } from 'discord-interactions'; 2 | import type { Env } from './types'; 3 | 4 | export class JsonResponse extends Response { 5 | constructor(body: unknown, init?: ResponseInit) { 6 | const jsonBody = JSON.stringify(body); 7 | const newInit = init || { 8 | headers: { 9 | 'content-type': 'application/json;charset=UTF-8', 10 | }, 11 | }; 12 | super(jsonBody, newInit); 13 | } 14 | } 15 | 16 | export async function interactionFollowup( 17 | message: string, 18 | interactionToken: string, 19 | env: Env, 20 | ) { 21 | const discordUrl = `https://discord.com/api/v10/webhooks/${env.DISCORD_APPLICATION_ID}/${interactionToken}`; 22 | const response = await fetch(discordUrl, { 23 | method: 'POST', 24 | headers: { 25 | 'Content-Type': 'application/json', 26 | }, 27 | body: JSON.stringify({ 28 | content: message, 29 | flags: 1 << 6, // ephemeral 30 | }), 31 | }); 32 | if (!response.ok) { 33 | throwFetchError(response as unknown as Response); 34 | } 35 | } 36 | 37 | export function interactionStatus(message: string) { 38 | return new JsonResponse({ 39 | type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, 40 | data: { 41 | content: message, 42 | flags: 1 << 6, // ephemeral 43 | }, 44 | }); 45 | } 46 | 47 | export async function throwFetchError(response: Response) { 48 | let errorText = `Error fetching ${response.url}: ${response.status} ${response.statusText}`; 49 | try { 50 | const error = await response.text(); 51 | if (error) { 52 | errorText = `${errorText} \n\n ${error}`; 53 | } 54 | } catch {} 55 | 56 | throw new FetchError(errorText, response); 57 | } 58 | 59 | export class FetchError extends Error { 60 | constructor( 61 | message: string, 62 | public response: Response, 63 | ) { 64 | super(message); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2021", 4 | "lib": ["es2021"], 5 | "module": "es2022", 6 | "moduleResolution": "node", 7 | "types": [ 8 | "@cloudflare/workers-types", 9 | ], 10 | "resolveJsonModule": true, 11 | "noEmit": true, 12 | "isolatedModules": true, 13 | "allowSyntheticDefaultImports": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": true, 16 | "skipLibCheck": true 17 | } 18 | } 19 | --------------------------------------------------------------------------------