From 4f54e2a874758b108688260b640216006181f5e0 Mon Sep 17 00:00:00 2001 From: LunaUrsa <1836049+LunaUrsa@users.noreply.github.com> Date: Mon, 15 Jan 2024 13:53:46 -0600 Subject: [PATCH] Add quote system --- assets/data/combinedDB.json | 75 ++- assets/data/psychonautDB.json | 108 +++- package.json | 2 +- src/discord/commands/guild/d.quote.ts | 508 ++++++++++++++++++ src/discord/commands/guild/m.quote.ts | 20 + src/discord/events/autocomplete.ts | 70 +++ src/global/utils/env.config.ts | 1 + .../20240115194928_quotes/migration.sql | 21 + src/prisma/tripbot/schema.prisma | 21 +- 9 files changed, 804 insertions(+), 22 deletions(-) create mode 100644 src/discord/commands/guild/d.quote.ts create mode 100644 src/discord/commands/guild/m.quote.ts create mode 100644 src/prisma/tripbot/migrations/20240115194928_quotes/migration.sql diff --git a/assets/data/combinedDB.json b/assets/data/combinedDB.json index 220a2459e..3b6e05446 100644 --- a/assets/data/combinedDB.json +++ b/assets/data/combinedDB.json @@ -19258,6 +19258,77 @@ } ] }, + { + "url": "https://psychonautwiki.org/wiki/Chlordiazepoxide", + "name": "Chlordiazepoxide", + "aliases": [ + "Librium" + ], + "aliasesStr": "Librium", + "toxicity": [ + "low toxicity", + "potentially lethal when mixed with depressants like alcohol or opioids" + ], + "roas": [ + { + "name": "Oral", + "dosage": [ + { + "name": "Threshold", + "value": "5 mg" + }, + { + "name": "Light", + "value": "5-10 mg" + }, + { + "name": "Common", + "value": "10-25 mg" + }, + { + "name": "Strong", + "value": "50-100 mg" + }, + { + "name": "Heavy", + "value": "100 mg" + } + ], + "duration": [ + { + "name": "Onset", + "value": "30-60 minutes" + }, + { + "name": "Peak", + "value": "120-180 minutes" + }, + { + "name": "Afterglow", + "value": "150-300 hours" + }, + { + "name": "Total", + "value": "24-60 hours" + } + ] + } + ], + "interactions": [ + { + "status": "Dangerous", + "name": "Depressant" + }, + { + "status": "Dangerous", + "name": "Dissociatives" + }, + { + "status": "Dangerous", + "name": "Stimulants" + } + ] + }, { "url": "https://psychonautwiki.org/wiki/Choline_bitartrate", "name": "Choline bitartrate", @@ -39322,10 +39393,6 @@ "url": "https://psychonautwiki.org/wiki/Oroxylin_A", "name": "Oroxylin A" }, - { - "url": "https://psychonautwiki.org/wiki/Orphenadrine", - "name": "Orphenadrine" - }, { "url": "https://psychonautwiki.org/wiki/Oxazepam", "name": "Oxazepam", diff --git a/assets/data/psychonautDB.json b/assets/data/psychonautDB.json index efd2009f1..ff3b9bac0 100644 --- a/assets/data/psychonautDB.json +++ b/assets/data/psychonautDB.json @@ -14628,6 +14628,99 @@ ], "roa": null }, + { + "url": "https://psychonautwiki.org/wiki/Chlordiazepoxide", + "name": "Chlordiazepoxide", + "summary": "", + "addictionPotential": null, + "toxicity": [ + "low toxicity", + "potentially lethal when mixed with depressants like alcohol or opioids" + ], + "crossTolerances": null, + "commonNames": [ + "Librium" + ], + "class": { + "chemical": [ + "Benzodiazepines" + ], + "psychoactive": [ + "Depressant" + ] + }, + "tolerance": null, + "uncertainInteractions": null, + "unsafeInteractions": null, + "dangerousInteractions": [ + { + "name": "Depressant" + }, + { + "name": "Dissociatives" + }, + { + "name": "Stimulants" + } + ], + "roa": { + "oral": { + "name": "oral", + "dose": { + "units": "mg", + "threshold": 5, + "heavy": 100, + "common": { + "min": 10, + "max": 25 + }, + "light": { + "min": 5, + "max": 10 + }, + "strong": { + "min": 50, + "max": 100 + } + }, + "duration": { + "afterglow": { + "min": 150, + "max": 300, + "units": "hours" + }, + "comeup": null, + "duration": null, + "offset": null, + "onset": { + "min": 30, + "max": 60, + "units": "minutes" + }, + "peak": { + "min": 120, + "max": 180, + "units": "minutes" + }, + "total": { + "min": 24, + "max": 60, + "units": "hours" + } + }, + "bioavailability": null + }, + "sublingual": null, + "buccal": null, + "insufflated": null, + "rectal": null, + "transdermal": null, + "subcutaneous": null, + "intramuscular": null, + "intravenous": null, + "smoked": null + } + }, { "url": "https://psychonautwiki.org/wiki/Choline_bitartrate", "name": "Choline bitartrate", @@ -30329,21 +30422,6 @@ "dangerousInteractions": null, "roa": null }, - { - "url": "https://psychonautwiki.org/wiki/Orphenadrine", - "name": "Orphenadrine", - "summary": "", - "addictionPotential": null, - "toxicity": null, - "crossTolerances": null, - "commonNames": null, - "class": null, - "tolerance": null, - "uncertainInteractions": null, - "unsafeInteractions": null, - "dangerousInteractions": null, - "roa": null - }, { "url": "https://psychonautwiki.org/wiki/Oxazepam", "name": "Oxazepam", diff --git a/package.json b/package.json index 8fccfbf3a..66df69314 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "db:validateSchema": "docker exec -it tripbot npx prisma validate", "db:generateClient": "npx prisma generate && docker exec -it tripbot npx prisma generate", "db:pushDev": "docker exec -it tripbot npx prisma db push && npm run tripbot:db:generate", - "db:migrateDev": "docker exec -it tripbot npx prisma migrate dev -n betterModeration", + "db:migrateDev": "docker exec -it tripbot npx prisma migrate dev -n quotes", "db:seed": "docker exec -it tripbot npx prisma db seed", "## PGADMIN ##": "", "pgadmin": "docker compose --project-name tripbot up -d --force-recreate --build tripbot_pgadmin", diff --git a/src/discord/commands/guild/d.quote.ts b/src/discord/commands/guild/d.quote.ts new file mode 100644 index 000000000..3bbc05107 --- /dev/null +++ b/src/discord/commands/guild/d.quote.ts @@ -0,0 +1,508 @@ +import { + ChatInputCommandInteraction, + Colors, + GuildMember, + MessageContextMenuCommandInteraction, + SlashCommandBuilder, + TextChannel, +} from 'discord.js'; +import { stripIndents } from 'common-tags'; +import { SlashCommand } from '../../@types/commandDef'; +import commandContext from '../../utils/context'; + +const F = f(__filename); + +const flavorText = [ + 'once famously declared,', + 'is forever known for saying,', + 'left us with the immortal words,', + 'proudly proclaimed,', + 'eternally etched these words into history,', + 'once articulated the thought,', + 'gave us the timeless utterance,', + 'voiced the memorable line,', + 'shared this nugget of wisdom,', + 'will always be remembered for stating,', + 'enlightened us with the words,', + 'left an indelible mark with the saying,', + 'immortalized in their words,', + 'spoke the now legendary phrase,', + 'echoed through time with the words,', + 'bequeathed us this piece of wisdom,', + 'cast these words into the annals of history,', + 'brought forth the insightful saying,', + 'cherished for their words,', + 'gifted us with the profound utterance,', +]; + +const successResponses = [ + "Quote saved! It's now etched in the annals of history.", + 'Let it be known that {target.displayName} said this. Witnessed and recorded!', + "Captured for posterity! This quote won't be forgotten.", + 'Engraved in the digital stone. Another {target.displayName} classic.', + 'Added to the vault of wisdom. Future generations will thank you.', + "Stamped and stored. The archives grow wiser with {target.displayName}'s words.", + 'This gem has been safely tucked away in the quote treasury.', + 'Quote logged. The sages of the future will ponder this one.', + 'Sealed into the annals of our digital history. Well said, {target.displayName}!', + "Quote recorded! It's now part of the collective digital consciousness.", + 'Preserved for eternity. Or at least until the next database purge.', + 'This quote is now part of the echo chamber of history.', + 'Safeguarded in the vault of virtual lore. Well articulated, {target.displayName}!', + "Cataloged under 'Brilliant Utterances by {target.displayName}'.", + 'Locked and loaded into the quote compendium. Future scholars, take note!', + "Quote snagged! It's now shimmering in the digital constellation of wisdom.", + "Ink's still wet, but it's official. {target.displayName}'s words are immortalized!", + 'Snatched from the ether and saved for all time. Well quipped, {target.displayName}!', + 'Et voilà! Another verbal masterpiece saved in the gallery of eloquence.', + "This quote's now riding the eternal waves of the data stream. Surf's up!", + 'Bottled and shelved in the library of digital musings. Cheers, {target.displayName}!', + "Zapped into the cybernetic memory banks. This one's a keeper!", + "Stamped with the seal of 'Absolutely Noteworthy'. The archives grow richer.", + 'Scribed onto the virtual parchment. The future thanks you for this wisdom, {target.displayName}.', + "Beam me up, Scotty! This quote's now aboard the starship of perpetuity.", + 'Nailed it! This quote has been hammered into the beams of digital history.', + 'Whisked away into the cloud of legendary utterances. Float on, wise words!', + 'And... snap! Captured this quote like a rare digital butterfly.', + 'Duly noted and saved. The oracle will be pleased with this one, {target.displayName}.', + 'Encased in the digital amber of time. This quote is now a relic of wisdom!', +]; + +const failResponses = [ + "Oops! This quote's already in the digital echo chamber.", + 'Too slow! Someone captured this wisdom before you.', + "Deja vu! Looks like this quote's already been snagged.", + "This gem's already been mined and stored in our vault.", + 'Hold up! This pearl of wisdom is already shining in our collection.', + "Echo! Echo! We've heard (and saved) this one before.", + "Looks like you're in a time loop. This quote's already here!", + "The early bird gets the worm, and this quote's already been gobbled up.", + "Duplicate detected! This one's a repeat performance.", + "Already on record! This quote's been around the block.", + "Rewind! We've seen this scene before in our quote archives.", + "Copy-paste error: This quote's a clone already living in our database.", + "Someone beat you to the punch. This quote's already in the ring.", + "History repeats itself, but this quote's repetition is a bit too soon.", + "This quote's a boomerang, already come back to us before.", + "Been there, saved that! This one's a déjà-quote.", + "It's a twin! We've already adopted this quote into our family.", + "The ink's barely dry, but this quote's already been written in our books.", + "Telepathy or coincidence? This quote's already been teleported to our servers.", + "Quote déjà-saved! This one's already in the digital vault.", + "Strike two! This quote's already hit a home run into our database.", + "This quote's echo is still bouncing in our halls. Already recorded!", + "A quote so nice, we saved it... oh wait, just once. It's already here.", + "Great minds think alike, and someone's already thought of saving this.", + "Your timing's like a rerun, we've seen this quote already.", + "This quote's like a favorite song on repeat, already played in our system.", + "Whoops, déjà-quoted! This one's already gracing our archives.", + 'Your quote submission is experiencing a case of double vision.', + "Blink twice if you've seen this. Yep, this quote's already in our sights.", + "Quote redundancy alert! This one's already living a cozy life in our database.", +]; + +async function get(interaction:ChatInputCommandInteraction) { + if (!interaction.guild) return; + const quote = interaction.options.getString('quote', true); + + log.debug(F, `Searching for quote: ${quote}`); + + const quoteData = await db.quotes.findFirst({ + where: { + quote: { + contains: quote, + }, + }, + }); + + if (!quoteData) { + log.debug(F, 'Quote not found'); + await interaction.reply({ + content: 'Quote not found!', + ephemeral: true, + }); + return; + } + + log.debug(F, 'Quote found!'); + + const authorData = await db.users.findFirstOrThrow({ + where: { + id: quoteData.user_id, + }, + }); + + if (!authorData.discord_id) return; // Just to type safe + + const target = await interaction.guild.members.fetch(authorData.discord_id); + + await interaction.reply({ + embeds: [{ + thumbnail: { + url: target.user.displayAvatarURL(), + }, + description: stripIndents`${target} ${flavorText[Math.floor(Math.random() * flavorText.length)]} + + > **${quoteData.quote}** + + - ${quoteData.url} + `, + color: target.displayColor, + timestamp: `${quoteData.date.toISOString()}`, + }], + ephemeral: false, + }); +} + +async function random(interaction:ChatInputCommandInteraction) { + if (!interaction.guild) return; + const quotes = await db.quotes.findMany({ + orderBy: { + id: 'desc', + }, + take: 1, + }); + + if (!quotes) { + await interaction.reply({ + content: 'No quotes found!', + ephemeral: true, + }); + return; + } + + // Get a random quote from the list + const quote = quotes[Math.floor(Math.random() * quotes.length)]; + + const authorData = await db.users.findFirstOrThrow({ + where: { + id: quote.user_id, + }, + }); + + const author = await interaction.guild.members.fetch(authorData.discord_id as string); + + await interaction.reply({ + embeds: [ + { + author: { + name: `${author.displayName} ${flavorText[Math.floor(Math.random() * flavorText.length)]}`, + icon_url: author.user.displayAvatarURL(), + url: quote.url, + }, + description: `**${quote.quote}**`, + timestamp: `${quote.date.toISOString()}`, + }, + ], + ephemeral: false, + }); +} + +async function del(interaction:ChatInputCommandInteraction) { + if (!interaction.guild) return; + if (!interaction.member) return; + const quote = interaction.options.getString('quote', true); + + log.debug(F, `Searching for quote: ${quote}`); + + const quoteData = await db.quotes.findFirst({ + where: { + quote: { + contains: quote, + }, + }, + }); + + if (!quoteData) { + log.debug(F, 'Quote not found'); + await interaction.reply({ + content: 'Quote not found!', + ephemeral: true, + }); + return; + } + + log.debug(F, 'Quote found!'); + + const targetData = await db.users.findFirstOrThrow({ + where: { + id: quoteData.user_id, + }, + }); + + if (!targetData.discord_id) return; // Just to type safe + + const target = await interaction.guild.members.fetch(targetData.discord_id); + + const guildData = await db.discord_guilds.findFirstOrThrow({ + where: { + id: interaction.guild.id, + }, + }); + + const actor = interaction.member as GuildMember; + + if (target.id !== interaction.user.id + || (guildData.role_moderator && !actor.roles.cache.has(guildData.role_moderator))) { + log.debug(F, 'User does not own quote'); + await interaction.reply({ + content: 'You do not own this quote! You can only delete your own quotes.', + ephemeral: true, + }); + return; + } + + await db.quotes.delete({ + where: { + id: quoteData.id, + }, + }); + + const quoteLog = await discordClient.channels.fetch(env.CHANNEL_QUOTE_LOG) as TextChannel; + quoteLog.send({ + embeds: [ + { + thumbnail: { + url: target.user.displayAvatarURL(), + }, + description: stripIndents`${target} + + > **${quoteData.quote}** + + - ${quoteData.url} + `, + footer: { + text: `Deleted by ${actor.displayName} (${actor.id})`, + icon_url: interaction.user.displayAvatarURL(), + }, + color: Colors.Red, + }, + ], + }); + + await interaction.reply({ + embeds: [{ + thumbnail: { + url: target.user.displayAvatarURL(), + }, + description: stripIndents`${target} + + > **${quoteData.quote}** + + - ${quoteData.url} + `, + footer: { + text: 'Deleted from the /quote database!', + }, + color: Colors.Red, + }], + ephemeral: true, + }); +} + +export async function quoteAdd(interaction:MessageContextMenuCommandInteraction) { + log.info(F, await commandContext(interaction)); + await interaction.deferReply({ ephemeral: true }); + if (!interaction.guild) return false; + if (interaction.guild.id !== env.DISCORD_GUILD_ID) return false; + await interaction.targetMessage.fetch(); // Fetch the message, just in case + + // Get the actor + const actor = interaction.member as GuildMember; + + // Get the target + const target = interaction.targetMessage.member; + + // IDK how this happens but making it type-safe + if (!target) { + log.debug(F, 'No target member found'); + await interaction.editReply({ + content: 'You can only save messages that have an author, or from the last 2 weeks!', + }); + return true; + } + + // Don't allow saving bot messages + if (target.user.bot) { + log.debug(F, 'Target is a bot'); + await interaction.editReply({ + content: 'You can only save messages from humans!', + }); + return true; + } + + // Don't allow saving your own messages + if (target.id === actor.id && !actor.id === env.DISCORD_OWNER_ID) { + log.debug(F, 'Target is the actor'); + await interaction.editReply({ + content: 'You can\'t save your own messages!', + }); + return true; + } + + // Don't allow people under level 10 to save quotes + const vipRoles = [ + env.ROLE_VIP_10, + env.ROLE_VIP_20, + env.ROLE_VIP_30, + env.ROLE_VIP_40, + env.ROLE_VIP_50, + env.ROLE_VIP_60, + env.ROLE_VIP_70, + env.ROLE_VIP_80, + env.ROLE_VIP_90, + env.ROLE_VIP_100, + ] + .map(role => actor.roles.cache.has(role)) // Check if the actor has any of these roles + .filter(role => role); // Filter out any non-truthy values + + // log.debug(F, `VIP Roles: ${vipRoles}`); + + if (vipRoles.length === 0) { + log.debug(F, 'Actor has no VIP roles'); + await interaction.editReply({ + content: 'You need to be at least level 10 to save quotes!', + }); + return true; + } + + // Check if the URL already exists in the database + const quoteExists = await db.quotes.findFirst({ + where: { + url: interaction.targetMessage.url, + }, + }); + if (quoteExists) { + log.debug(F, 'Quote already exists'); + await interaction.editReply({ + content: failResponses[Math.floor(Math.random() * failResponses.length)], + }); + return true; + } + + log.debug(F, `All checks passed, saving quote from ${target.displayName} (${target.id})`); + + const actorData = await db.users.upsert({ + where: { discord_id: actor.id }, + create: { discord_id: actor.id }, + update: {}, + }); + + const targetData = await db.users.upsert({ + where: { discord_id: target.id }, + create: { discord_id: target.id }, + update: {}, + }); + + const quoteData = await db.quotes.create({ + data: { + user_id: targetData.id, + quote: interaction.targetMessage.content, + url: interaction.targetMessage.url, + date: interaction.targetMessage.createdAt, + created_by: actorData.id, + }, + }); + + await interaction.targetMessage.reply({ + embeds: [{ + description: stripIndents`${successResponses[Math.floor(Math.random() * successResponses.length)] + .replace('{target.displayName}', target.displayName)} + `, + // footer: { + // text: 'Saved to the /quote database!', + // }, + color: Colors.Green, + }], + }); + + const quoteLog = await discordClient.channels.fetch(env.CHANNEL_QUOTE_LOG) as TextChannel; + quoteLog.send({ + embeds: [ + { + thumbnail: { + url: target.user.displayAvatarURL(), + }, + description: stripIndents`${target} + + > **${quoteData.quote}** + + - ${quoteData.url} + `, + footer: { + text: `Saved by ${actor.displayName} (${actor.id})`, + icon_url: interaction.user.displayAvatarURL(), + }, + color: Colors.Green, + }, + ], + }); + + await interaction.editReply({ + embeds: [{ + thumbnail: { + url: target.user.displayAvatarURL(), + }, + description: stripIndents`${target} + + > **${quoteData.quote}** + + - ${quoteData.url} + `, + footer: { + text: 'Saved to the /quote database!', + }, + color: Colors.Green, + }], + }); + + return true; +} + +export const dQuote: SlashCommand = { + data: new SlashCommandBuilder() + .setName('quote') + .setDescription('Manage quotes') + .addSubcommand(subcommand => subcommand + .setDescription('Search quotes!') + .addStringOption(option => option.setName('quote') + .setDescription('Which quote? Type to search!') + .setAutocomplete(true) + .setRequired(true)) + .addUserOption(option => option.setName('user') + .setDescription('Which user?')) + .setName('get')) + .addSubcommand(subcommand => subcommand + .setDescription('Get a random quote!') + .setName('random')) + .addSubcommand(subcommand => subcommand + .setDescription('Delete your own quote records!') + .addStringOption(option => option.setName('quote') + .setDescription('Which quote? Type to search!') + .setAutocomplete(true) + .setRequired(true)) + .setName('delete')), + async execute(interaction) { + log.info(F, await commandContext(interaction)); + switch (interaction.options.getSubcommand()) { + case 'get': + await get(interaction); + break; + case 'random': + await random(interaction); + break; + case 'delete': + await del(interaction); + break; + default: + await interaction.reply({ + content: 'Unknown subcommand!', + ephemeral: true, + }); + break; + } + return true; + }, +}; + +export default dQuote; diff --git a/src/discord/commands/guild/m.quote.ts b/src/discord/commands/guild/m.quote.ts new file mode 100644 index 000000000..a6ee703e5 --- /dev/null +++ b/src/discord/commands/guild/m.quote.ts @@ -0,0 +1,20 @@ +import { + ContextMenuCommandBuilder, +} from 'discord.js'; +import { + ApplicationCommandType, +} from 'discord-api-types/v10'; +import { MessageCommand } from '../../@types/commandDef'; +import { quoteAdd } from './d.quote'; + +export const mQuote: MessageCommand = { + data: new ContextMenuCommandBuilder() + .setName('Save Quote') + .setType(ApplicationCommandType.Message), + async execute(interaction) { + await quoteAdd(interaction); + return true; + }, +}; + +export default mQuote; diff --git a/src/discord/events/autocomplete.ts b/src/discord/events/autocomplete.ts index faf6434e0..358d06923 100644 --- a/src/discord/events/autocomplete.ts +++ b/src/discord/events/autocomplete.ts @@ -550,6 +550,73 @@ async function autocompleteAiNames(interaction:AutocompleteInteraction) { } } +async function autocompleteQuotes(interaction:AutocompleteInteraction) { + const options = { + shouldSort: true, + keys: [ + 'quote', + ], + }; + + let whereClause = {}; + + // If the user option is filled in, find the user's ID and use that to filter the quotes + const user = interaction.options.get('user'); + if (user) { + // log.debug(F, `User option: ${user.value}`); + const userValue = user.value as string; + const userData = await db.users.upsert({ + where: { discord_id: userValue }, + create: { discord_id: userValue }, + update: {}, + }); + whereClause = { + user_id: userData.id, + }; + } + + // log.debug(F, `whereClause: ${JSON.stringify(whereClause, null, 2)}`); + const quoteList = await db.quotes.findMany({ + select: { + quote: true, + user_id: true, + url: true, + date: true, + }, + where: whereClause, + }); + + // log.debug(F, `quoteList: ${quoteList.length}`); + + const fuse = new Fuse(quoteList, options); + const focusedValue = interaction.options.getFocused(); + // log.debug(F, `focusedValue: ${focusedValue}`); + const results = fuse.search(focusedValue); + // log.debug(F, `Autocomplete results: ${results.length}`); + // log.debug(F, `Autocomplete results: ${JSON.stringify(results, null, 2)}`); + if (results.length > 0) { + const top25 = results.slice(0, 20); + const listResults = top25.map(choice => ({ + name: (choice.item as any).quote.slice(0, 100), + value: (choice.item as any).quote.slice(0, 100), + })); + // log.debug(F, `list_results: ${listResults}`); + await interaction.respond(listResults); + } else if (focusedValue !== '') { + await interaction.respond([ + { name: 'No results found', value: 'No results found' }, + ]); + } else { + const initialQuotes = quoteList.slice(0, 25) as { + quote: string; + }[]; + const listResults = initialQuotes.map(choice => ({ name: choice.quote.slice(0, 100), value: choice.quote.slice(0, 100) })); + // log.debug(F, `list_results: ${listResults}`); + // log.debug(F, `Returing ${listResults.length} quotes`); + await interaction.respond(listResults); + } +} + export default autocomplete; /** * Handles autocomplete information @@ -557,6 +624,7 @@ export default autocomplete; * @param {Client} discordClient * @return {Promise} */ + export async function autocomplete(interaction:AutocompleteInteraction):Promise { // log.debug(F, `Autocomplete requested for: ${interaction.commandName}`); if (interaction.commandName === 'pill-id') { @@ -579,6 +647,8 @@ export async function autocomplete(interaction:AutocompleteInteraction):Promise< if (focusedOption === 'name') { autocompleteAiNames(interaction); } + } else if (interaction.commandName === 'quote') { + await autocompleteQuotes(interaction); } else { // If you don't need a specific autocomplete, return a list of drug names await autocompleteDrugNames(interaction); } diff --git a/src/global/utils/env.config.ts b/src/global/utils/env.config.ts index 68a1b1efa..572de563f 100644 --- a/src/global/utils/env.config.ts +++ b/src/global/utils/env.config.ts @@ -178,6 +178,7 @@ export const env = { CHANNEL_AILOG: isProd ? '1137781426444574801' : '1137747287607623800', CHANNEL_AIIMAGELOG: isProd ? '1175554418414989504' : '1175521105054810214', CHANNEL_AIMOD_LOG: isProd ? '1169746977811091577' : '1169747347165687838', + CHANNEL_QUOTE_LOG: isProd ? '1196464856690335824' : '1196465107421634580', CATEGORY_RADIO: isProd ? '981069604665327646' : '1052634090819551436', CHANNEL_SYNTHWAVERADIO: isProd ? '1050099921811939359' : '1052634114693541898', diff --git a/src/prisma/tripbot/migrations/20240115194928_quotes/migration.sql b/src/prisma/tripbot/migrations/20240115194928_quotes/migration.sql new file mode 100644 index 000000000..15a1d6022 --- /dev/null +++ b/src/prisma/tripbot/migrations/20240115194928_quotes/migration.sql @@ -0,0 +1,21 @@ +-- CreateTable +CREATE TABLE "quotes" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "user_id" UUID NOT NULL, + "quote" TEXT NOT NULL, + "url" TEXT NOT NULL, + "date" TIMESTAMP(3) NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "created_by" UUID NOT NULL, + + CONSTRAINT "quotes_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "quotes_url_unique" ON "quotes"("url"); + +-- AddForeignKey +ALTER TABLE "quotes" ADD CONSTRAINT "quotes_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "quotes" ADD CONSTRAINT "quotes_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; diff --git a/src/prisma/tripbot/schema.prisma b/src/prisma/tripbot/schema.prisma index e398d6f13..b2b88afa9 100644 --- a/src/prisma/tripbot/schema.prisma +++ b/src/prisma/tripbot/schema.prisma @@ -165,6 +165,8 @@ model users { user_tickets_user_tickets_user_idTousers user_tickets[] @relation("user_tickets_user_idTousers") ai_usage ai_usage[] ai_images ai_images[] + quotes_quotes_user_idTousers quotes[] @relation("UserQuote") + quotes_quotes_created_byTousers quotes[] @relation("QuoteCreator") } model discord_guilds { @@ -305,7 +307,7 @@ model rpg_inventory { model user_actions { id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid user_id String @db.Uuid - guild_id String @default("179641883222474752") + guild_id String @default("179641883222474752") type user_action_type ban_evasion_related_user String? @db.Uuid description String? @@ -320,7 +322,7 @@ model user_actions { users_user_actions_repealed_byTousers users? @relation("user_actions_repealed_byTousers", fields: [repealed_by], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "useractions_repealedby_foreign") users_user_actions_user_idTousers users @relation("user_actions_user_idTousers", fields: [user_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "useractions_userid_foreign") - discord_guilds discord_guilds @relation(fields: [guild_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "appeals_guildid_foreign") + discord_guilds discord_guilds @relation(fields: [guild_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "appeals_guildid_foreign") } model user_drug_doses { @@ -474,6 +476,21 @@ model ai_images { users users @relation(fields: [user_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "aiimages_userid_foreign") } +model quotes { + id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + user_id String @db.Uuid + quote String + url String + date DateTime + created_at DateTime @default(now()) @db.Timestamptz(6) + created_by String @db.Uuid + + user users @relation("UserQuote", fields: [user_id], references: [id], onDelete: NoAction, onUpdate: NoAction) + creator users @relation("QuoteCreator", fields: [created_by], references: [id], onDelete: NoAction, onUpdate: NoAction) + + @@unique([url], map: "quotes_url_unique") +} + enum bridge_status { PENDING ACTIVE