From 6c6fe79599f09c36055d9410174dae0fce5d95ab Mon Sep 17 00:00:00 2001 From: ZeldaFan0225 <78901316+ZeldaFan0225@users.noreply.github.com> Date: Sat, 5 Aug 2023 21:22:53 +0200 Subject: [PATCH] party improvements --- changelog.md | 12 ++++++++ config.md | 7 ++++- package-lock.json | 16 +++++------ package.json | 4 +-- src/classes/client.ts | 13 +++++++-- src/commands/advanced_generate.ts | 9 +++++- src/commands/end_party.ts | 12 +++++++- src/commands/generate.ts | 13 +++++++-- src/commands/party.ts | 46 +++++++++++++++++++++++++++++-- src/index.ts | 8 +++--- src/types.ts | 9 +++++- template.config.json | 7 ++++- 12 files changed, 129 insertions(+), 27 deletions(-) diff --git a/changelog.md b/changelog.md index ff8d3a6..74f7a8b 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,17 @@ # Changelog +## V3.2.0 + +- pay for every generation in a party, let the bot create a shared key which everybody will automatically when creating an image in the party! +- create a list of required words for generations in your party + +**Migrating** + +MAKE SURE THE FOLLOWING STEPS ARE DONE IN ORDER +- run `ALTER TABLE parties ADD COLUMN shared_key VARCHAR(100)` on your postgres database before deploying this or any future version when using a database +- run `ALTER TABLE parties ADD COLUMN wordlist text[] NOT NULL DEFAULT '{}'` on your postgres database + + ## V3.1.0 - save horde user id for easy lookup diff --git a/config.md b/config.md index 6d6b80c..0b3a8da 100644 --- a/config.md +++ b/config.md @@ -199,7 +199,8 @@ Here you can see an explanation of what which option does "enabled": Whether the remix action is enabled (BOOLEAN), "mention_roles": The roles to mention when a party is started (ARRAY OF ROLE IDS), "default": { - "recurring": The default for recurring awards (BOOLEAN) + "recurring": The default for recurring awards (BOOLEAN), + "pay_for_generations": The default for paying for generations (BOOLEAN) }, "user_restrictions": { "award": { @@ -209,6 +210,10 @@ Here you can see an explanation of what which option does "duration": { "min": (BOOLEAN) *5, "max": (BOOLEAN) *5, + }, + "wordlist": { + "min": (BOOLEAN), + "max": (BOOLEAN), } } } diff --git a/package-lock.json b/package-lock.json index 355bdd3..3a6f2a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "zeldafan_discord_bot", - "version": "3.1.0", + "version": "3.2.0", "lockfileVersion": 2, "requires": true, "packages": { @@ -10,7 +10,7 @@ "license": "ISC", "dependencies": { "@thunder04/supermap": "^3.0.2", - "@zeldafan0225/ai_horde": "^5.0.4", + "@zeldafan0225/ai_horde": "^5.0.5", "centra": "^2.5.0", "discord.js": "^14.8.0", "pg": "^8.8.0", @@ -162,9 +162,9 @@ } }, "node_modules/@zeldafan0225/ai_horde": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@zeldafan0225/ai_horde/-/ai_horde-5.0.4.tgz", - "integrity": "sha512-z/NMPZhgx4lWX33OFfFHiaKsYAUHJDlmGXaDJteQ5hjh9sdNEHWrl2C1pu5W53J1X5U4h5XzSRFGezUcl1K+8A==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@zeldafan0225/ai_horde/-/ai_horde-5.0.5.tgz", + "integrity": "sha512-uDClLDc2KA0MHHQqE1LW+L7lfP47YWV99BfR5N7eGOgsF/zgbJqO8pnz0/ZaoNxKOmbVJAL95h6CpNTnuB96ZQ==", "dependencies": { "@thunder04/supermap": "^3.0.2" }, @@ -703,9 +703,9 @@ } }, "@zeldafan0225/ai_horde": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@zeldafan0225/ai_horde/-/ai_horde-5.0.4.tgz", - "integrity": "sha512-z/NMPZhgx4lWX33OFfFHiaKsYAUHJDlmGXaDJteQ5hjh9sdNEHWrl2C1pu5W53J1X5U4h5XzSRFGezUcl1K+8A==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@zeldafan0225/ai_horde/-/ai_horde-5.0.5.tgz", + "integrity": "sha512-uDClLDc2KA0MHHQqE1LW+L7lfP47YWV99BfR5N7eGOgsF/zgbJqO8pnz0/ZaoNxKOmbVJAL95h6CpNTnuB96ZQ==", "requires": { "@thunder04/supermap": "^3.0.2" } diff --git a/package.json b/package.json index 77987c0..4fff4a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zeldafan_discord_bot", - "version": "3.1.0", + "version": "3.2.0", "description": "", "main": "dist/index.js", "scripts": { @@ -20,7 +20,7 @@ "homepage": "https://github.com/ZeldaFan0225/AI_Horde_Discord#readme", "dependencies": { "@thunder04/supermap": "^3.0.2", - "@zeldafan0225/ai_horde": "^5.0.4", + "@zeldafan0225/ai_horde": "^5.0.5", "centra": "^2.5.0", "discord.js": "^14.8.0", "pg": "^8.8.0", diff --git a/src/classes/client.ts b/src/classes/client.ts index abae8f7..c41e7e5 100644 --- a/src/classes/client.ts +++ b/src/classes/client.ts @@ -7,6 +7,7 @@ import {existsSync, mkdirSync, writeFileSync} from "fs" import { Pool } from "pg"; import crypto from "crypto" import Centra from "centra"; +import { AIHorde, SharedKeyDetails } from "@zeldafan0225/ai_horde"; export class AIHordeClient extends Client { commands: Store; @@ -148,13 +149,21 @@ export class AIHordeClient extends Client { return p.rows[0]! } - async cleanUpParties(database?: Pool) { + async cleanUpParties(ai_horde_manager: AIHorde, database?: Pool) { const expired_parties = await database?.query("DELETE FROM parties WHERE ends_at <= CURRENT_TIMESTAMP RETURNING *").catch(console.error) if(!expired_parties?.rowCount) return; for(let party of expired_parties.rows) { const channel = await this.channels.fetch(party.channel_id).catch(console.error) if(!channel?.id || channel?.type !== ChannelType.PublicThread) continue; - await channel?.send({content: `This party ended.\n${party.users?.length} users participated.\nThanks to <@${party.creator_id}> for hosting this party`}) + let usagestats: SharedKeyDetails = {} + if(party.shared_key) { + const usertoken = await this.getUserToken(party.creator_id, database) + usagestats = await ai_horde_manager.getSharedKey(party.shared_key, {token: usertoken}).catch(console.error) || {} + + await ai_horde_manager.deleteSharedKey(party.shared_key, {token: usertoken}).catch(console.error) + } + await channel?.send({content: `This party ended.\n${party.users?.length} users participated.${usagestats ? `\n${usagestats.utilized} kudos have been spent by <@${party.creator_id}> only for generations in this party` : ""}\nThanks to <@${party.creator_id}> for hosting this party`}) + } } diff --git a/src/commands/advanced_generate.ts b/src/commands/advanced_generate.ts index f86133b..7070ee3 100644 --- a/src/commands/advanced_generate.ts +++ b/src/commands/advanced_generate.ts @@ -311,6 +311,13 @@ export default class extends Command { prompt = style.prompt.slice().replace("{p}", prompt) prompt = prompt.replace("{np}", !negative_prompt || prompt.includes("###") ? negative_prompt : `###${negative_prompt}`) + if(party && party.wordlist.length) { + if(ctx.client.config.advanced?.dev) { + console.log(party.wordlist) + } + if(!party.wordlist.every(w => prompt.toLowerCase().includes(w))) return ctx.error({error: "Your prompt does not include all required words"}) + } + if(ctx.client.config.advanced?.dev) { console.log(img?.height) console.log(img?.width) @@ -318,7 +325,7 @@ export default class extends Command { console.log(width) } - const token = user_token || ctx.client.config.default_token || "0000000000" + const token = party?.shared_key || user_token || ctx.client.config.default_token || "0000000000" let img_data: Buffer | undefined if(img) { let img_data_res = await Centra(img.url, "GET") diff --git a/src/commands/end_party.ts b/src/commands/end_party.ts index 58fc627..04a3443 100644 --- a/src/commands/end_party.ts +++ b/src/commands/end_party.ts @@ -1,6 +1,7 @@ import { SlashCommandBuilder } from "discord.js"; import { Command } from "../classes/command"; import { CommandContext } from "../classes/commandContext"; +import { SharedKeyDetails } from "@zeldafan0225/ai_horde"; const command_data = new SlashCommandBuilder() .setName("end_party") @@ -28,9 +29,18 @@ export default class extends Command { if(!party_data?.rowCount) return ctx.error({error: "Unable to end party"}) ctx.client.cache.delete(`party-${ctx.interaction.channelId}`) + let usagestats: SharedKeyDetails = {} + + if(party.shared_key) { + const usertoken = await ctx.client.getUserToken(ctx.interaction.user.id, ctx.database) + usagestats = await ctx.ai_horde_manager.getSharedKey(party.shared_key, {token: usertoken}).catch(console.error) || {} + + await ctx.ai_horde_manager.deleteSharedKey(party.shared_key, {token: usertoken}).catch(console.error) + } + await ctx.interaction.reply({content: "Party ended.", ephemeral: true}) ctx.interaction.channel?.send({ - content: `The party police showed up and broke down this party.\n${party_data.rows[0].users?.length} users participated.\nThanks to <@${party.creator_id}> for hosting this party` + content: `The party police showed up and broke down this party.\n${party_data.rows[0].users?.length} users participated.${usagestats ? `\n${usagestats.utilized} kudos have been spent by <@${party.creator_id}> only for generations in this party` : ""}\nThanks to <@${party.creator_id}> for hosting this party` }) } } \ No newline at end of file diff --git a/src/commands/generate.ts b/src/commands/generate.ts index aacad05..3d47656 100644 --- a/src/commands/generate.ts +++ b/src/commands/generate.ts @@ -167,6 +167,13 @@ export default class extends Command { prompt = style.prompt.slice().replace("{p}", prompt) prompt = prompt.replace("{np}", !negative_prompt || prompt.includes("###") ? negative_prompt : `###${negative_prompt}`) + + if(party && party.wordlist.length) { + if(ctx.client.config.advanced?.dev) { + console.log(party.wordlist) + } + if(!party.wordlist.every(w => prompt.toLowerCase().includes(w))) return ctx.error({error: "Your prompt does not include all required words"}) + } if(keep_ratio && img?.width && img?.height) { const ratio = img?.width/img?.height @@ -185,7 +192,7 @@ export default class extends Command { console.log(width) } - const token = user_token || ctx.client.config.default_token || "0000000000" + const token = party?.shared_key || user_token || ctx.client.config.default_token || "0000000000" let img_data: Buffer | undefined if(img) { let img_data_res = await Centra(img.url, "GET") @@ -234,9 +241,9 @@ export default class extends Command { const generation_start = await ctx.ai_horde_manager.postAsyncImageGenerate(generation_data, {token}) .catch((e) => { if(ctx.client.config.advanced?.dev) console.error(e) - return e; + return e.message; }) - if(!generation_start || !generation_start.id) return ctx.error({error: `Unable to start generation: ${generation_start.message}`}); + if(!generation_start || !generation_start.id) return ctx.error({error: `Unable to start generation: ${generation_start}`}); if (ctx.client.config.logs?.enabled) { diff --git a/src/commands/party.ts b/src/commands/party.ts index de70d38..60fe67a 100644 --- a/src/commands/party.ts +++ b/src/commands/party.ts @@ -48,6 +48,16 @@ const command_data = new SlashCommandBuilder() .setName("recurring") .setDescription("If users get rewarded for each generation or only their first") ) + .addBooleanOption( + new SlashCommandBooleanOption() + .setName("pay_for_generations") + .setDescription("Whether to pay for the generations users make") + ) + .addStringOption( + new SlashCommandStringOption() + .setName("wordlist") + .setDescription("Set a comma separated list of words the users prompt has to include") + ) } @@ -69,6 +79,8 @@ export default class extends Command { const award = ctx.interaction.options.getInteger("award", true) const duration = ctx.interaction.options.getInteger("duration", true) const recurring = !!(ctx.interaction.options.getBoolean("recurring") ?? ctx.client.config.party?.default?.recurring) + const pay = !!(ctx.interaction.options.getBoolean("pay_for_generations") ?? ctx.client.config.party?.default?.pay_for_generations) + const wordlist = (ctx.interaction.options.getString("wordlist") ?? "").split(",").map(w => w.trim().toLowerCase()) const style_raw = ctx.interaction.options.getString("style") ?? ctx.client.config.generate?.default?.style ?? "raw" const style = ctx.client.horde_styles[style_raw.toLowerCase()] @@ -82,6 +94,13 @@ export default class extends Command { if(!style) return ctx.error({error: "A valid style is required"}) if(ctx.client.config.generate?.blacklisted_styles?.includes(style_raw.toLowerCase())) return ctx.error({error: "The chosen style is blacklisted"}) + if(ctx.client.config.party.user_restrictions?.wordlist) { + if( + ctx.client.config.party.user_restrictions?.wordlist.min || 0 > wordlist.length || + ctx.client.config.party.user_restrictions?.wordlist.max && ctx.client.config.party.user_restrictions?.wordlist.max < wordlist.length + ) return ctx.error({error: `Your wordlist must be between ${ctx.client.config.party.user_restrictions?.wordlist.min || "no"} and ${ctx.client.config.party.user_restrictions?.wordlist.max || "unlimited"} words`}) + } + await ctx.interaction.deferReply({ephemeral: true}) const thread = await ctx.interaction.channel.threads.create({ @@ -90,13 +109,30 @@ export default class extends Command { }).catch(console.error) if(!thread?.id) return ctx.error({error: "Unable to start party"}) - const party = await ctx.database.query(`INSERT INTO parties (channel_id, guild_id, creator_id, ends_at, style, award, recurring) VALUES ($1, $2, $3, CURRENT_TIMESTAMP + interval '${duration} day', $4, $5, $6) RETURNING *`, [ + let shared_key_id: string | null = null + + if(pay) { + const token = await ctx.client.getUserToken(ctx.interaction.user.id, ctx.database) + + const shared_key = await ctx.ai_horde_manager.putSharedKey({ + kudos: 100000, + expiry: duration, + name: `Party ${name}` + }, {token}).catch(console.error) + + if(shared_key?.id) shared_key_id = shared_key.id + if(ctx.client.config.advanced?.dev) console.log(shared_key_id) + } + + const party = await ctx.database.query(`INSERT INTO parties (channel_id, guild_id, creator_id, ends_at, style, award, recurring, shared_key, wordlist) VALUES ($1, $2, $3, CURRENT_TIMESTAMP + interval '${duration} day', $4, $5, $6, $7, $8) RETURNING *`, [ thread.id, thread.guildId, ctx.interaction.user.id, style_raw.toLowerCase(), award, - recurring + recurring, + shared_key_id, + wordlist ]).catch(console.error) if(!party?.rowCount) { @@ -105,7 +141,11 @@ export default class extends Command { } const start = await thread.send({ - content: `<@${ctx.interaction.user.id}> started the party "${name}" with the style "${style_raw}".\nYou will get ${award} kudos for ${recurring ? `every generation` : `your first generation`}.\nThe party ends \n\n${ctx.client.config.party.mention_roles?.length ? ctx.client.config.party.mention_roles.map(r => `<@&${r}>`).join(" ") : ""}` + content: `<@${ctx.interaction.user.id}> started the party "${name}" with the style "${style_raw}".\nYou will get ${award} kudos for ${recurring ? `every generation` : `your first generation`}.\nThe party ends ${wordlist.length ? `\nThe prompt has to include the words: ${wordlist.join(",")}` : ""}${pay && shared_key_id ? "\nThe party creator will pay for all generations 🥳" : ""}\n\n${ctx.client.config.party.mention_roles?.length ? ctx.client.config.party.mention_roles.map(r => `<@&${r}>`).join(" ") : ""}`, + allowedMentions: { + users: [ctx.interaction.user.id], + roles: ctx.client.config.party.mention_roles + } }).catch(console.error) await start?.pin().catch(console.error) diff --git a/src/index.ts b/src/index.ts index 660bec6..3d643f8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ import { handleComponents } from "./handlers/componentHandler"; import { handleModals } from "./handlers/modalHandler"; import { Pool } from "pg" import { handleAutocomplete } from "./handlers/autocompleteHandler"; -import {AIHorde} from "@zeldafan0225/ai_horde"; +import { AIHorde } from "@zeldafan0225/ai_horde"; import { handleContexts } from "./handlers/contextHandler"; import {existsSync, mkdirSync} from "fs" import { handleMessageReact } from "./handlers/messageReact"; @@ -41,7 +41,7 @@ if(client.config.use_database !== false) { connection.connect().then(async () => { await connection!.query("CREATE TABLE IF NOT EXISTS user_tokens (index SERIAL, id VARCHAR(100) PRIMARY KEY, token VARCHAR(100) NOT NULL, horde_id int NOT NULL DEFAULT 0)") - await connection!.query("CREATE TABLE IF NOT EXISTS parties (index SERIAL, channel_id VARCHAR(100) PRIMARY KEY, guild_id VARCHAR(100) NOT NULL, creator_id VARCHAR(100) NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, ends_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, style VARCHAR(1000) NOT NULL, award INT NOT NULL DEFAULT 1, recurring BOOLEAN NOT NULL DEFAULT false, users VARCHAR(100)[] NOT NULL DEFAULT '{}')") + await connection!.query("CREATE TABLE IF NOT EXISTS parties (index SERIAL, channel_id VARCHAR(100) PRIMARY KEY, guild_id VARCHAR(100) NOT NULL, creator_id VARCHAR(100) NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, ends_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, style VARCHAR(1000) NOT NULL, award INT NOT NULL DEFAULT 1, recurring BOOLEAN NOT NULL DEFAULT false, users VARCHAR(100)[] NOT NULL DEFAULT '{}', shared_key VARCHAR(100), wordlist text[] NOT NULL DEFAULT '{}')") await connection!.query("CREATE TABLE IF NOT EXISTS pending_kudos (index SERIAL, unique_id VARCHAR(200) PRIMARY KEY, target_id VARCHAR(100) NOT NULL, from_id VARCHAR(100) NOT NULL, amount int NOT NULL, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)") }).catch(console.error); @@ -86,8 +86,8 @@ client.on("ready", async () => { if(client.config.party?.enabled && !client.config.generate?.enabled) throw new Error("When party is enabled the /generate command also needs to be enabled") if(client.config.party?.enabled && connection) { - await client.cleanUpParties(connection) - setInterval(async () => await client.cleanUpParties(connection), 1000 * 60 * 5) + await client.cleanUpParties(ai_horde_manager, connection) + setInterval(async () => await client.cleanUpParties(ai_horde_manager, connection), 1000 * 60 * 5) } }) diff --git a/src/types.ts b/src/types.ts index f2f6f66..3e8637a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -86,7 +86,9 @@ export interface Party { style: string, award: number, recurring: boolean, - users: string[] + shared_key?: string, + users: string[], + wordlist: string[] } export interface LORAFetchResponse { @@ -391,6 +393,7 @@ export interface Config { mention_roles?: string[], default?: { recurring?: boolean + pay_for_generations?: boolean }, user_restrictions?: { award?: { @@ -400,6 +403,10 @@ export interface Config { duration?: { min?: number, max?: number + }, + wordlist?: { + min?: number, + max?: number } } } diff --git a/template.config.json b/template.config.json index 62fa144..a83b811 100644 --- a/template.config.json +++ b/template.config.json @@ -224,7 +224,8 @@ "1234567890123456789" ], "default": { - "recurring": false + "recurring": false, + "pay_for_generations": false }, "user_restrictions": { "award": { @@ -234,6 +235,10 @@ "duration": { "min": 1, "max": 30 + }, + "wordlist": { + "min": 0, + "max": 100 } } }