From 66a37d03c28143f124753b674e3069996fb13ec7 Mon Sep 17 00:00:00 2001 From: atridadl Date: Sun, 26 Nov 2023 02:38:31 -0700 Subject: [PATCH] Commands updates --- package.json | 10 +-- pnpm-lock.yaml | 68 +++++++++-------- src/.env.example | 3 +- src/commands/borf.ts | 4 +- src/commands/credits.ts | 51 +++++++++++++ src/commands/dad.ts | 2 +- src/commands/fancypic.ts | 2 +- src/commands/pic.ts | 158 +++++++++++++++++++++++++++++---------- src/commands/quack.ts | 2 +- 9 files changed, 217 insertions(+), 83 deletions(-) create mode 100644 src/commands/credits.ts diff --git a/package.json b/package.json index 3efff02..26d17be 100644 --- a/package.json +++ b/package.json @@ -17,18 +17,18 @@ "@skyra/env-utilities": "^1.2.1", "colorette": "^2.0.20", "discord.js": "^14.14.1", - "openai": "^4.19.0" + "openai": "^4.20.0" }, "devDependencies": { - "@flydotio/dockerfile": "^0.4.10", + "@flydotio/dockerfile": "^0.4.11", "@sapphire/cli": "^1.8.0", "@sapphire/prettier-config": "^2.0.0", "@sapphire/ts-config": "^5.0.0", - "@types/node": "^20.9.2", - "@types/ws": "^8.5.9", + "@types/node": "^20.10.0", + "@types/ws": "^8.5.10", "prettier": "^3.1.0", "tsc-watch": "^6.0.4", - "typescript": "^5.2.2" + "typescript": "^5.3.2" }, "scripts": { "sapphire": "sapphire", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e456abe..d70baf2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,13 +30,13 @@ dependencies: specifier: ^14.14.1 version: 14.14.1 openai: - specifier: ^4.19.0 - version: 4.19.0 + specifier: ^4.20.0 + version: 4.20.0 devDependencies: '@flydotio/dockerfile': - specifier: ^0.4.10 - version: 0.4.10 + specifier: ^0.4.11 + version: 0.4.11 '@sapphire/cli': specifier: ^1.8.0 version: 1.8.0 @@ -47,20 +47,20 @@ devDependencies: specifier: ^5.0.0 version: 5.0.0 '@types/node': - specifier: ^20.9.2 - version: 20.9.2 + specifier: ^20.10.0 + version: 20.10.0 '@types/ws': - specifier: ^8.5.9 - version: 8.5.9 + specifier: ^8.5.10 + version: 8.5.10 prettier: specifier: ^3.1.0 version: 3.1.0 tsc-watch: specifier: ^6.0.4 - version: 6.0.4(typescript@5.2.2) + version: 6.0.4(typescript@5.3.2) typescript: - specifier: ^5.2.2 - version: 5.2.2 + specifier: ^5.3.2 + version: 5.3.2 packages: @@ -122,7 +122,7 @@ packages: '@discordjs/rest': 2.2.0 '@discordjs/util': 1.0.2 '@sapphire/async-queue': 1.5.0 - '@types/ws': 8.5.9 + '@types/ws': 8.5.10 '@vladfrangu/async_event_emitter': 2.2.2 discord-api-types: 0.37.61 tslib: 2.6.2 @@ -144,8 +144,8 @@ packages: colorette: 2.0.20 dev: true - /@flydotio/dockerfile@0.4.10: - resolution: {integrity: sha512-cyxxVTzPX7qRD3/yq24eiIsSKlUFPcBjbIQmWeuXn3qS2rSdYMioUmDISchfGE7S75cEdL/baK0AJP2q7+aw6w==} + /@flydotio/dockerfile@0.4.11: + resolution: {integrity: sha512-L52UAfrOhmAn3T4TxpeRofQOSO+Kctg+uraB4nLzo4mvvh+4Z7HYxSi7Dnq0Kirz+xx6fDIc4OMNT1EdaORecA==} engines: {node: '>=16.0.0'} hasBin: true dependencies: @@ -188,7 +188,7 @@ packages: resolution: {integrity: sha512-gKgTkWIBgkG0c+V3ALXeoD7XeciAYQtHNewjltSMaxCLF/wsy6NFj6xxqdEeSJMF40tcfUJ7F44r2DR5r3K8Eg==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} dependencies: - discord-api-types: 0.37.63 + discord-api-types: 0.37.65 dev: false /@sapphire/discord.js-utilities@7.1.2: @@ -291,7 +291,7 @@ packages: engines: {node: '>=v16.0.0', npm: '>=8.0.0'} dependencies: tslib: 2.6.2 - typescript: 5.2.2 + typescript: 5.3.2 dev: true /@sapphire/utilities@3.13.0: @@ -310,25 +310,31 @@ packages: /@types/node-fetch@2.6.9: resolution: {integrity: sha512-bQVlnMLFJ2d35DkPNjEPmd9ueO/rh5EiaZt2bhqiSarPjZIuIV6bPQVqcrEyvNo+AfTrRGVazle1tl597w3gfA==} dependencies: - '@types/node': 20.9.2 + '@types/node': 20.10.0 form-data: 4.0.0 dev: false - /@types/node@18.18.10: - resolution: {integrity: sha512-luANqZxPmjTll8bduz4ACs/lNTCLuWssCyjqTY9yLdsv1xnViQp3ISKwsEWOIecO13JWUqjVdig/Vjjc09o8uA==} + /@types/node@18.18.13: + resolution: {integrity: sha512-vXYZGRrSCreZmq1rEjMRLXJhiy8MrIeVasx+PCVlP414N7CJLHnMf+juVvjdprHyH+XRy3zKZLHeNueOpJCn0g==} dependencies: undici-types: 5.26.5 dev: false - /@types/node@20.9.2: - resolution: {integrity: sha512-WHZXKFCEyIUJzAwh3NyyTHYSR35SevJ6mZ1nWwJafKtiQbqRTIKSRcw3Ma3acqgsent3RRDqeVwpHntMk+9irg==} + /@types/node@20.10.0: + resolution: {integrity: sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==} dependencies: undici-types: 5.26.5 + /@types/ws@8.5.10: + resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} + dependencies: + '@types/node': 20.10.0 + /@types/ws@8.5.9: resolution: {integrity: sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg==} dependencies: - '@types/node': 20.9.2 + '@types/node': 20.10.0 + dev: false /@vladfrangu/async_event_emitter@2.2.2: resolution: {integrity: sha512-HIzRG7sy88UZjBJamssEczH5q7t5+axva19UbZLO6u0ySbYPrwzWiXBcC0WuHyhKKoeCyneH+FvYzKQq/zTtkQ==} @@ -484,8 +490,8 @@ packages: resolution: {integrity: sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==} dev: false - /discord-api-types@0.37.63: - resolution: {integrity: sha512-WbEDWj/1JGCIC1oCMIC4z9XbYY8PrWpV5eqFFQymJhJlHMqgIjqoYbU812X5oj5cwbRrEh6Va4LNLumB2Nt6IQ==} + /discord-api-types@0.37.65: + resolution: {integrity: sha512-CQHW3Nu04LEHIj1Xps/sfGhTdrowilxnek2tirpLhwvrmgmLr1C6A+4JFLs+0kJMH2IX2QgDyA9GfNehqN+xPQ==} dev: false /discord.js@14.14.1: @@ -788,11 +794,11 @@ packages: mimic-fn: 4.0.0 dev: true - /openai@4.19.0: - resolution: {integrity: sha512-cJbl0noZyAaXVKBTMMq6X5BAvP1pm2rWYDBnZes99NL+Zh5/4NmlAwyuhTZEru5SqGGZIoiYKeMPXy4bm9DI0w==} + /openai@4.20.0: + resolution: {integrity: sha512-VbAYerNZFfIIeESS+OL9vgDkK8Mnri55n+jN0UN/HZeuM0ghGh6nDN6UGRZxslNgyJ7XmY/Ca9DO4YYyvrszGA==} hasBin: true dependencies: - '@types/node': 18.18.10 + '@types/node': 18.18.13 '@types/node-fetch': 2.6.9 abort-controller: 3.0.0 agentkeepalive: 4.5.0 @@ -949,7 +955,7 @@ packages: resolution: {integrity: sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==} dev: false - /tsc-watch@6.0.4(typescript@5.2.2): + /tsc-watch@6.0.4(typescript@5.3.2): resolution: {integrity: sha512-cHvbvhjO86w2aGlaHgSCeQRl+Aqw6X6XN4sQMPZKF88GoP30O+oTuh5lRIJr5pgFWrRpF1AgXnJJ2DoFEIPHyg==} engines: {node: '>=12.12.0'} hasBin: true @@ -960,14 +966,14 @@ packages: node-cleanup: 2.1.2 ps-tree: 1.2.0 string-argv: 0.3.2 - typescript: 5.2.2 + typescript: 5.3.2 dev: true /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - /typescript@5.2.2: - resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} + /typescript@5.3.2: + resolution: {integrity: sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==} engines: {node: '>=14.17'} hasBin: true dev: true diff --git a/src/.env.example b/src/.env.example index dc6a04b..cb27056 100644 --- a/src/.env.example +++ b/src/.env.example @@ -1,3 +1,4 @@ # Tokens DISCORD_TOKEN= -OPENAI_API_KEY= \ No newline at end of file +OPENAI_API_KEY= +STABILITY_API_KEY= \ No newline at end of file diff --git a/src/commands/borf.ts b/src/commands/borf.ts index 107e72c..948a865 100644 --- a/src/commands/borf.ts +++ b/src/commands/borf.ts @@ -28,11 +28,11 @@ export class UserCommand extends Command { private async sendBorf(interactionOrMessage: Message | Command.ChatInputCommandInteraction | Command.ContextMenuCommandInteraction) { const dogResponse = await fetch('https://dog.ceo/api/breeds/image/random'); - const dogData = await dogResponse.json(); + const dogData = (await dogResponse.json()) as { message: string; status: string }; interactionOrMessage instanceof Message ? await interactionOrMessage.channel.send({ - content: dogData.status === 'success' ? dogData.message : 'Error: I had troubles fetching perfect puppies for you... :(' + content: dogData?.status === 'success' ? dogData.message : 'Error: I had troubles fetching perfect puppies for you... :(' }) : await interactionOrMessage.reply({ content: dogData.status === 'success' ? dogData.message : 'Error: I had troubles fetching perfect puppies for you... :(', diff --git a/src/commands/credits.ts b/src/commands/credits.ts new file mode 100644 index 0000000..ecbafd9 --- /dev/null +++ b/src/commands/credits.ts @@ -0,0 +1,51 @@ +import { ApplyOptions } from '@sapphire/decorators'; +import { Command } from '@sapphire/framework'; +import { Message } from 'discord.js'; + +// @ts-ignore +@ApplyOptions({ + description: 'Check how many credits I have left!' +}) +export class UserCommand extends Command { + // Register Chat Input and Context Menu command + public override registerApplicationCommands(registry: Command.Registry) { + registry.registerChatInputCommand((builder) => builder.setName(this.name).setDescription(this.description)); + } + + // Message command + public async messageRun(message: Message) { + return this.credits(message); + } + + // Chat Input (slash) command + public async chatInputRun(interaction: Command.ChatInputCommandInteraction) { + return this.credits(interaction); + } + + private async credits(interactionOrMessage: Message | Command.ChatInputCommandInteraction | Command.ContextMenuCommandInteraction) { + const askMessage = + interactionOrMessage instanceof Message + ? await interactionOrMessage.channel.send({ content: 'šŸ¤” Thinking... šŸ¤”' }) + : await interactionOrMessage.reply({ content: 'šŸ¤” Thinking... šŸ¤”', fetchReply: true }); + + const creditCountResponse = await fetch(`https://api.stability.ai/v1/user/balance`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${process.env.STABILITY_API_KEY}` + } + }); + + const balance = ((await creditCountResponse.json()) as { credits: number }).credits || 0; + + const content = `I have ${balance} credits remaining for image generation!`; + + if (interactionOrMessage instanceof Message) { + return askMessage.edit({ content }); + } + + return interactionOrMessage.editReply({ + content: content + }); + } +} diff --git a/src/commands/dad.ts b/src/commands/dad.ts index 5d0fc97..74d0d3a 100644 --- a/src/commands/dad.ts +++ b/src/commands/dad.ts @@ -32,7 +32,7 @@ export class UserCommand extends Command { Accept: 'application/json' } }); - const jokeData = await jokeResponse.json(); + const jokeData = (await jokeResponse.json()) as { status: number; joke: string }; interactionOrMessage instanceof Message ? await interactionOrMessage.channel.send({ diff --git a/src/commands/fancypic.ts b/src/commands/fancypic.ts index d8a514f..1cc7913 100644 --- a/src/commands/fancypic.ts +++ b/src/commands/fancypic.ts @@ -52,7 +52,7 @@ export class UserCommand extends Command { prompt, n: 1, size: '1024x1024', - quality: 'hd' + quality: 'standard' }); const imageUrl = response.data[0].url || ''; diff --git a/src/commands/pic.ts b/src/commands/pic.ts index c4e7bc1..b2699da 100644 --- a/src/commands/pic.ts +++ b/src/commands/pic.ts @@ -1,17 +1,15 @@ import { ApplyOptions } from '@sapphire/decorators'; import { Args, BucketScope, Command } from '@sapphire/framework'; import { AttachmentBuilder, Message } from 'discord.js'; -import OpenAI from 'openai'; -const openai = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY -}); +// This is literally the worlds messiest TS code. Please don't judge me... // @ts-ignore @ApplyOptions({ - description: 'Generate a pic every 4 minutes!', - options: ['prompt'], - cooldownDelay: 400_000, + description: 'Make a picture... but high res!', + options: ['prompt', 'number of pictures'], + // 10mins + cooldownDelay: 100, cooldownLimit: 1, // Yes... I did hardcode myself. cooldownFilteredUsers: ['83679718401904640'], @@ -27,65 +25,143 @@ export class UserCommand extends Command { .addStringOption((option) => option.setName('prompt').setDescription('The prompt you will use to generate an image!').setRequired(true) ) + .addStringOption((option) => + option + .setName('amount') + .setDescription('The number of images you would like to generate. Maximum 2.') + .setChoices( + ...[ + { name: '1', value: '1' }, + { name: '2', value: '2' } + ] + ) + ) ); } // Message command public async messageRun(message: Message, args: Args) { - return this.pic(message, args.getOption('prompt') || 'NOTHING'); + const amount = Math.abs(Number(args.getOption('amount'))); + return this.picHr(message, args.getOption('prompt') || 'Scold me for not passing any prompt in.', amount <= 2 ? amount : 1); } // Chat Input (slash) command public async chatInputRun(interaction: Command.ChatInputCommandInteraction) { - return this.pic(interaction, interaction.options.getString('prompt') || 'NOTHING'); + const amount = Number(interaction.options.getString('amount')); + return this.picHr(interaction, interaction.options.getString('prompt') || 'NOTHING', amount || 1); } - private async pic(interactionOrMessage: Message | Command.ChatInputCommandInteraction | Command.ContextMenuCommandInteraction, prompt: string) { + private async picHr( + interactionOrMessage: Message | Command.ChatInputCommandInteraction | Command.ContextMenuCommandInteraction, + prompt: string, + amount: number + ) { const askMessage = interactionOrMessage instanceof Message ? await interactionOrMessage.channel.send({ content: 'šŸ¤” Thinking... šŸ¤”' }) : await interactionOrMessage.reply({ content: 'šŸ¤” Thinking... šŸ¤”', fetchReply: true }); - try { - const response = await openai.images.generate({ - model: 'dall-e-3', - prompt, - n: 1, - size: '1024x1024', - quality: 'standard' + const creditCountResponse = await fetch(`https://api.stability.ai/v1/user/balance`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${process.env.STABILITY_API_KEY}` + } + }); + + const balance = ((await creditCountResponse.json()) as { credits: number }).credits || 0; + + if (balance > 5) { + const imageGenResponse = await fetch(`https://api.stability.ai/v1/generation/stable-diffusion-xl-1024-v1-0/text-to-image`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + Authorization: `Bearer ${process.env.STABILITY_API_KEY}` + }, + body: JSON.stringify({ + text_prompts: [ + { + text: prompt + } + ], + cfg_scale: 6, + clip_guidance_preset: 'FAST_BLUE', + height: 1024, + width: 1024, + samples: amount, + steps: 32, + seed: Number(String(interactionOrMessage.member?.user.id).substring(0, 5)) || 0 + }) }); - const imageUrl = response.data[0].url || ''; - // get an array buffer - const imageBuffer = await fetch(imageUrl).then((r) => r.arrayBuffer()); - - const imageAttachment: AttachmentBuilder[] = []; - - imageAttachment.push( - new AttachmentBuilder(Buffer.from(new Uint8Array(imageBuffer)), { - name: 'himbot_response.jpg', - description: `An image generated by Himbot using the prompt: ${prompt}` - }) - ); - - const content = `Prompt: ${prompt}:`; - - if (interactionOrMessage instanceof Message) { - return askMessage.edit({ content, files: imageAttachment }); + interface GenerationResponse { + artifacts: Array<{ + base64: string; + seed: number; + finishReason: string; + }>; } - return interactionOrMessage.editReply({ - content, - files: imageAttachment - }); - } catch (error) { - const content = "Sorry, I can't complete the prompt for: " + prompt + '\n' + error; + if (!imageGenResponse.ok) { + const content = `Sorry, I can't complete the prompt for: ${prompt}`; + + if (interactionOrMessage instanceof Message) { + return askMessage.edit({ content }); + } + + return interactionOrMessage.editReply({ + content: content + }); + } else { + const responseJSON = (await imageGenResponse.json()) as GenerationResponse; + const imageAttachment: AttachmentBuilder[] = []; + + for (let i = 0; i < responseJSON.artifacts.length; i++) { + imageAttachment.push( + new AttachmentBuilder(Buffer.from(responseJSON.artifacts[i].base64, 'base64'), { + name: 'response.jpg', + description: "Himbot's Response" + }) + ); + } + + const newCreditCountResponse = await fetch(`https://api.stability.ai/v1/user/balance`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${process.env.STABILITY_API_KEY}` + } + }); + + const newBalance = ((await newCreditCountResponse.json()) as { credits: number }).credits || 0; + + const content = + `Credits Used: ${balance - newBalance}\nPrompt: ${prompt}${ + balance <= 300 + ? `\n\nāš ļøI am now at ${balance} credits. If you'd like to help fund this command, please type "/support" for details!` + : '' + }` || 'ERROR!'; + + if (interactionOrMessage instanceof Message) { + return askMessage.edit({ content, files: imageAttachment }); + } + + return interactionOrMessage.editReply({ + content, + files: imageAttachment + }); + } + } else { + const content = `Oops! We're out of credits for this. If you'd like to help fund this command, please type "/support" for details!`; if (interactionOrMessage instanceof Message) { return askMessage.edit({ content }); } - return interactionOrMessage.editReply({ content }); + return interactionOrMessage.editReply({ + content: content + }); } } } diff --git a/src/commands/quack.ts b/src/commands/quack.ts index 9d158b4..8e04d98 100644 --- a/src/commands/quack.ts +++ b/src/commands/quack.ts @@ -28,7 +28,7 @@ export class UserCommand extends Command { private async sendQuack(interactionOrMessage: Message | Command.ChatInputCommandInteraction | Command.ContextMenuCommandInteraction) { const duckResponse = await fetch('https://random-d.uk/api/v2/quack'); - const duckData = await duckResponse.json(); + const duckData = (await duckResponse.json()) as { url: string }; interactionOrMessage instanceof Message ? await interactionOrMessage.channel.send({