diff --git a/.gitignore b/.gitignore index 85e6ccf..c48a3c4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ node_modules .env test.ts .DS_Store -*.db \ No newline at end of file +*.db +*.sql +settings.json \ No newline at end of file diff --git a/src/DBUtils.ts b/src/DBUtils.ts deleted file mode 100644 index 524f9d8..0000000 --- a/src/DBUtils.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { User, Wallet, GachaInv } from "./JeffreyDB" - -/** - * Checks if the given userID is in the "User" table. - * - * @param {string} userID - Users Discord ID - * @returns {Promise}- True or False (whether or not their userID is in "User" table) - */ -export async function checkUser(userID: string): Promise { - - const user = await User.findOne({ where: { userid: userID } }); - if (user) { - console.log(`user ${user.userid} found in Users`); - return true; - } else { - console.log(`user ${userID} NOT found in Users`); - return false; - } -} - -/** - * Adds a row in the "User" table, which contains a users discord ID and discord Username. - * - * @param {string} userID - Users Discord ID - * @param {string} username - Users Discord Username(not displayname) - */ -export async function addUser(userID: string, username: string): Promise { - - await User.create({ userid: userID, username: username }); - console.log(`added ${userID} to Users`); -} - -/** - * Searches for the 'balance' associated the given users Discord ID. - * - * If no balance was found, it calls another function to create a new row in "Wallet"- - * -which contains the users Discord ID, and 'balance' defaults to 0. - * - * (balance is used in a monetary context $$$) - * - * @param {string} userID - Users Discord ID - * @returns {Promise} - Returns the users current balance in "Wallet" table. - * Returns 0 if no number was found (default value). - */ -export async function checkBalance(userID: string): Promise { - const userBalance = await Wallet.findOne({ where: { userid: userID } }); - if (userBalance) { - console.log(`${userID} has a balance of ${userBalance.balance}`); - return userBalance.balance; - } else { - await findOrAddUserWallet(userID); - return 0; - } -} - -/** - * Add or Subtract the 'balance' associated with the users Discord ID- - * -in the "Wallet" table by 'amount'. - * - * @param userID - Users Discord ID - * @param amount - Positive or Negative integer to Add or Subtract 'balance' by - */ -export async function addOrSubtractBalance(userID: string, amount: number): Promise { - await Wallet.increment({ balance: amount }, { where: { userid: userID } }); - console.log(`${userID}'s wallet has been changed by: ${amount}`); -} - -/** - * Checks if there is a row in the "Wallet" table that contains the users Discord ID- - * -creates one if it is not found. - * - * @param {string} userID - Users Discord ID - */ -export async function findOrAddUserWallet(userID: string): Promise { - await Wallet.findOrCreate({ where: { userid: userID } }); - console.log(`Found or created Wallet for ${userID}`); -} - -/** - * Checks for a row in GachaInv that contains both 'userID' and 'gachaURL'. - * Looking for if the user has previously aquired this gacha.(ie: if its a duplicate) - * - * @param {string} userID - Users Discord ID - * @param {string} gachaURL - URL associated with a specific gacha item (picture). - * @returns {Promise} - True or False (whether or not userID is in the same row as gachaURL in "GachaInv" table) - */ -export async function checkIfUserHasGachaInv(userID: string, gachaURL: string): Promise { - const user = await GachaInv.findOne({ where: { userid: userID, gachas: gachaURL } }); - if (user) { - console.log(`found ${userID} in GachaInvs`); - return true; - } else { - console.log(`did NOT find ${userID} in GachaInvs`); - return false; - } -} - -/** - * Adds a row in "GachaInv" table containing the users Disord ID, their new gacha picture, and setting amt to 1. - * This function must be called only when we know this row does not already exist (ie: call the checkGachaInv function before this). - * - * @param userID - Users Discord ID - * @param gachaURL - URL associated with a specific gacha item (picture). - */ -export async function addNewGacha(userID: string, gachaURL: string): Promise { - await GachaInv.create({ userid: userID, gachas: gachaURL, amt: 1 }); - console.log(`${userID} added to GachaInvs with their new ${gachaURL}`); -} - -/** - * Increases the amount ('amt') column in the row that contains both userID and gachaURL. - * Function should only be called when it is confirmed that a row exists with 'userID' and 'gachaURL' (ie: the 'checkGachaInv' function). - * - * @example - * (Wallet table) - * table starts with: | userID: 8743284 | gachas: https:/sajdjioafh.png | amt: 1 | - * table becomes: | userID: 8743284 | gachas: https:/sajdjioafh.png | amt: 2 | - * - * @param userID - Users Discord ID - * @param gachaURL - URL associated with a specific gacha item (picture). - */ -export async function gachaLvlUp(userID: string, gachaURL: string): Promise { - await GachaInv.increment({ amt: 1 }, { where: { userid: userID, gachas: gachaURL } }); - console.log(`increased ${gachaURL}'s level by 1 for ${userID}!`); -} - -/** - * Checks the 'amt' number column that is in the row of the users Discord ID and Gacha URL/ID. - * - * @param {string} userID - Users Discord ID - * @param {string} gachaURL - URL associated with a specific gacha item (picture). - * @returns {Promise} - The 'amt' column in the row containing 'userID' and 'gachaURL' - * Returns 0 if no 'amt' was found.(default value is 1, so 0 is impossible for 'amt') - */ -export async function checkGachaLevel(userID: string, gachaURL: string): Promise { - const level = await GachaInv.findOne({ where: { userid: userID, gachas: gachaURL } }); - if (level) { - return level.amt; - } else { - return 0; - } -} \ No newline at end of file diff --git a/src/JeffreyDB.ts b/src/JeffreyDB.ts deleted file mode 100644 index c27ef60..0000000 --- a/src/JeffreyDB.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { Sequelize, DataTypes, Model } from 'sequelize'; - -export const sequelize = new Sequelize({ - dialect: 'sqlite', - storage: './JeffreyDB.db', - logging: false, -}); - -// Creates the "User" table with: -// @example -// | userid:'667951424704872450' | username: 'jeffreyhassalide' | -export class User extends Model { - declare userid: string; - declare username: string; -} -User.init({ - userid: { - type: DataTypes.STRING, - allowNull: false, - primaryKey: true, - }, - username: { - type: DataTypes.STRING, - allowNull: false, - }, -}, { sequelize }); - -// Creates the "Wallet" table with -// @example -// | userid: 667951424704872450 | balance: 10 | -export class Wallet extends Model { - declare userid: string; - declare balance: number; -} -Wallet.init({ - userid: { - type: DataTypes.STRING, - allowNull: false, - references: { - model: User, - key: 'userid', - }, - }, - balance: { - type: DataTypes.INTEGER, - allowNull: false, - defaultValue: 0, - } -}, { sequelize }); -User.hasOne(Wallet, { foreignKey: 'userid', sourceKey: 'userid' }); - -// Creates the "GachaInv" table with -// @example -// | userid: 667951424704872450 | gachas: https:/gachaexample.png | amt: 2 | -export class GachaInv extends Model { - declare userid: string; - declare gachas: string; - declare amt: number; -} -GachaInv.init({ - userid: { - type: DataTypes.STRING, - references: { - model: User, - key: 'userid', - }, - }, - gachas: { - type: DataTypes.STRING, - allowNull: false, - }, - amt: { - type: DataTypes.INTEGER, - allowNull: false, - defaultValue: 1, - }, -}, { sequelize } -); -User.hasMany(GachaInv, { foreignKey: 'userid', sourceKey: 'userid' }); - -// Contains'test' and 'sync' -export const DB = { - /** - * test: - * Checks if it has access to the database file - */ - test: async (): Promise => { - try { - await sequelize.authenticate(); - console.log('Connection has been established successfully.'); - } catch (error) { - console.error('Unable to connect to the database:', error); - } - }, -/** - * sync: - * Tries to sync all database tables with the database file, - * which allows it to build upon the information previously stored in their respective tables after bot resets. - */ - sync: async (): Promise => { - try { - await User.sync(); - console.log(`UsersDB synced`); - - await Wallet.sync(); - console.log(`UserWallets synced`); - - await GachaInv.sync(); - console.log(`GachaInvs synced`); - } catch { - console.log('Failed to sync table(s)'); - } - } -}; \ No newline at end of file diff --git a/src/JeffreyGacha.ts b/src/JeffreyGacha.ts deleted file mode 100644 index 5543253..0000000 --- a/src/JeffreyGacha.ts +++ /dev/null @@ -1,131 +0,0 @@ - -type GachaURLType = { - link: string; - name: string; - description: string; -}; -//Object containing the 4 rarities, 'Common', 'Uncommon', 'Rare' and 'Legendary'. -//Each rarity is an array of gacha objects, containing the link(URL), name and description of all gachas. -export const JeffreyGachaURLs: { [key: string]: GachaURLType[] } = { - Common: [ - { link: 'https://imgur.com/iSHcsOd.png', name: '', description: '' }, - { link: 'https://imgur.com/Oyh1TiC.png', name: '', description: '' }, - { link: 'https://imgur.com/hr1tx64.png', name: '', description: '' }, - { link: 'https://imgur.com/xQqwrWD.png', name: '', description: '' }, - { link: 'https://imgur.com/4mrEWuV.png', name: '', description: '' }, - { link: 'https://imgur.com/cSprAX4.png', name: '', description: '' }, - { link: 'https://imgur.com/DaCVzq8.png', name: '', description: '' }, - { link: 'https://imgur.com/Z2LaGwY.png', name: '', description: '' }, - { link: 'https://imgur.com/mx70oY2.png', name: '', description: '' }, - { link: 'https://imgur.com/AivmZ0T.png', name: '', description: '' }, - { link: 'https://imgur.com/6dD6wHv.png', name: '', description: '' }, - { link: 'https://imgur.com/22niPvw.png', name: '', description: '' }, - { link: 'https://imgur.com/sqnH5tg.png', name: '', description: '' }, - { link: 'https://imgur.com/nXo9YbZ.png', name: '', description: '' }, - { link: 'https://imgur.com/po28ZPo.png', name: '', description: '' }, - { link: 'https://imgur.com/zEfh9G1.png', name: '', description: '' }, - { link: 'https://imgur.com/MsftbNA.png', name: '', description: '' }, - { link: 'https://imgur.com/GJFRw0N.png', name: '', description: '' }, - { link: 'https://imgur.com/Qgi4yeQ.png', name: '', description: '' }, - { link: 'https://imgur.com/bpVQ6xz.png', name: '', description: '' }, - { link: 'https://imgur.com/DdhzNO2.png', name: '', description: '' }, - { link: 'https://imgur.com/AvqpwfX.png', name: '', description: '' }, - { link: 'https://imgur.com/RuOtxcv.png', name: '', description: '' }, - { link: 'https://imgur.com/6WkntdW.png', name: '', description: '' }, - { link: 'https://imgur.com/40M1KOh.png', name: '', description: '' }, - { link: 'https://imgur.com/ZtPZZV9.png', name: '', description: '' } - ], - - Uncommon: [ - { link: 'https://imgur.com/txgkO4S.png', name: '', description: '' }, - { link: 'https://imgur.com/gqbRfhW.png', name: '', description: '' }, - { link: 'https://imgur.com/qYIxVan.png', name: '', description: '' }, - { link: 'https://imgur.com/KBwG10J.png', name: '', description: '' }, - { link: 'https://imgur.com/dord5jv.png', name: '', description: '' }, - { link: 'https://imgur.com/WGDO515.png', name: '', description: '' }, - { link: 'https://imgur.com/JujU88m.png', name: '', description: '' }, - { link: 'https://imgur.com/6ur3hgK.png', name: '', description: '' }, - { link: 'https://imgur.com/J1Ozvgw.png', name: '', description: '' }, - { link: 'https://imgur.com/DQDZFjA.png', name: '', description: '' }, - { link: 'https://imgur.com/15p7sDo.png', name: '', description: '' }, - { link: 'https://imgur.com/ASOaLa7.png', name: '', description: '' }, - { link: 'https://imgur.com/kSo1xYB.png', name: '', description: '' }, - { link: 'https://imgur.com/vWkhS1P.png', name: '', description: '' }, - { link: 'https://imgur.com/FirGg1T.png', name: '', description: '' }, - { link: 'https://imgur.com/WVAcNOm.png', name: '', description: '' }, - { link: 'https://imgur.com/57yMtrl.png', name: '', description: '' }, - { link: 'https://imgur.com/j00IjET.png', name: '', description: '' }, - { link: 'https://imgur.com/DnWGWJS.png', name: '', description: '' }, - { link: 'https://imgur.com/OqOol5X.png', name: '', description: '' }, - { link: 'https://imgur.com/xryAFJi.png', name: '', description: '' }, - { link: 'https://imgur.com/WYGcMzt.png', name: '', description: '' }, - { link: 'https://imgur.com/jhIC3Js.png', name: '', description: '' }, - { link: 'https://imgur.com/Qh7TDY3.png', name: '', description: '' }, - { link: 'https://imgur.com/NaKE5Y2.png', name: '', description: '' }, - { link: 'https://imgur.com/M2wJBt9.png', name: '', description: '' }, - { link: 'https://imgur.com/voMHJ9n.png', name: '', description: '' }, - { link: 'https://imgur.com/z81zoZ9.png', name: '', description: '' }, - { link: 'https://imgur.com/mQB4xHr.png', name: '', description: '' } - ], - - Rare: [ - { link: 'https://imgur.com/eNx2Ntg.png', name: '', description: '' }, - { link: 'https://imgur.com/moIgaas.png', name: '', description: '' }, - { link: 'https://imgur.com/M2wJBt9.png', name: '', description: '' }, - { link: 'https://imgur.com/NaKE5Y2.png', name: '', description: '' }, - { link: 'https://imgur.com/KBwG10J.png', name: '', description: '' }, - { link: 'https://imgur.com/D1cuR7k.png', name: '', description: '' }, - { link: 'https://imgur.com/rt6lIPt.png', name: '', description: '' }, - { link: 'https://imgur.com/vvWZ384.png', name: '', description: '' }, - { link: 'https://imgur.com/UFNSYrP.png', name: '', description: '' }, - { link: 'https://imgur.com/CKfOGtA.png', name: '', description: '' }, - { link: 'https://imgur.com/dVGsAIo.png', name: '', description: '' }, - { link: 'https://imgur.com/JujU88m.png', name: '', description: '' }, - { link: 'https://imgur.com/KnTrE2e.png', name: '', description: '' }, - { link: 'https://imgur.com/yjh3yZg.png', name: '', description: '' }, - { link: 'https://imgur.com/BbtiZwy.png', name: '', description: '' }, - { link: 'https://imgur.com/LijfPss.png', name: '', description: '' }, - { link: 'https://imgur.com/GnnuMI5.png', name: '', description: '' }, - { link: 'https://imgur.com/eGPRhm5.png', name: '', description: '' }, - { link: 'https://imgur.com/l8uOmAq.png', name: '', description: '' } - ], - - Legendary: [ - { link: 'https://imgur.com/miHJetv.png', name: 'Radiant Jeffrey', description: 'Our Lord and Savior Jeffrey' }, - { link: 'https://imgur.com/tbS4faL.png', name: 'Radiant Jeffrey', description: 'OK FINE ILL FEED YOU NOW!' }, - { link: 'https://imgur.com/5QfM3zQ.png', name: 'Dead Jeffrey', description: 'RIP. Fly high king' }, - { link: 'https://imgur.com/xhWXOIA.png', name: 'Box Jeffrey', description: 'Wow, soooo original Jeffrey. Never seen a cat do THAT before.' }, - { link: 'https://imgur.com/okvejbp.png', name: 'Disturbed Jeffrey', description: 'I don\'t think he\'s a fan...' }, - { link: 'https://imgur.com/sdLYctZ.png', name: 'Cuddle Jeffrey', description: 'Wittle Cutie!' }, - { link: 'https://imgur.com/CYvTDSC.png', name: 'Not Jeffrey', description: 'Hey, that\'s not Jeffrey!' }, - { link: 'https://imgur.com/jmKtrlf.png', name: 'Peaceful Jeffrey', description: 'That\'s about as peacful as its gonna get...' }, - { link: 'https://imgur.com/Y1i2gMP.png', name: 'Attack Jeffrey', description: 'That bite\'s lethal!' }, - { link: 'https://imgur.com/3aWCqqe.png', name: 'Caught Jeffrey', description: 'Hey, what going on over here!' }, - { link: 'https://imgur.com/eTGaGLM.png', name: 'Lap-Cat Jeffrey', description: 'He\'s been know to peruse a lap or two.' }, - { link: 'https://imgur.com/7YdKWjS.png', name: 'GateKeeper Jeffrey', description: 'Come on Jeffrey, that\'s not very nice!' } - ] - -} -/** - * Takes a Legendary gacha URL and returns an array containing the link(url), name and description of that gacha. - * - * @param {string} gacha - A Legendary gacha image URL - * @returns {string[]} - gachaInfo, contains legendary info (link, name and description) - */ -export async function displayLegendary(gacha: string): Promise { - const legendary = JeffreyGachaURLs['Legendary']; - for(let i = 0; i < legendary.length; i++){ - if(legendary[i].link === gacha){ - const gachaInfo = [ - legendary[i].name, - legendary[i].name, - legendary[i].description - ]; - console.log(gachaInfo); - return gachaInfo; - } - } - return null; -} - - diff --git a/src/commands/balance.ts b/src/commands/balance.ts index 8394f6b..2abe411 100644 --- a/src/commands/balance.ts +++ b/src/commands/balance.ts @@ -1,6 +1,5 @@ import { SlashCommandBuilder, EmbedBuilder, ChatInputCommandInteraction, User } from 'discord.js'; -import { checkUser, checkBalance, addOrSubtractBalance, addUser, findOrAddUserWallet } from '../DBUtils'; -import { replyWithEmbed } from '../utils'; +import { addOrSubtractWallet, findOrAddToUser, checkOrStartWallet } from '../databse/DBMain'; export const Balance = { info: new SlashCommandBuilder() @@ -24,11 +23,7 @@ export const Balance = { const member = interaction.options.getUser('member'); const add = interaction.options.getInteger('add'); - let target: User; - let targetBalance: number; - let startingBalance: number; - let higherUp = false; //checks if non-higher up attempted to use the 'add' option if (add) { @@ -42,6 +37,7 @@ export const Balance = { const commandUser = await interaction.guild.members.fetch(userID); + let higherUp = false; for (const currentRole of role) { if (!currentRole) { console.log(`ERROR finding higher up role(s) for balance command`); @@ -52,14 +48,15 @@ export const Balance = { break; } } - if(!higherUp){ - interaction.reply('Only officer+ can modify member balances!'); + if (!higherUp) { + await interaction.reply('Only officer+ can modify member balances!'); return; } } - //whos(target) balance to check - + // whos balance to check (target) = //'member' option if one is specified. Otherwise, defaults to the command user. + let target: User; if (!member || member.id === userID) { target = interaction.user; } else { @@ -67,45 +64,42 @@ export const Balance = { } //checks for target in Users, otherwise adds them - const userInUsers = await checkUser(target.id); - if (!userInUsers) { - await addUser(target.id, target.username); - } - const userInWallet = await findOrAddUserWallet(target.id); + await findOrAddToUser(target.id, target.username, target.displayName); + const startingBalance = await checkOrStartWallet(target.id); + let displayBalance = startingBalance; //checks targets current balance, then adds 'add' to it (can be negative) if (add !== null) { - startingBalance = await checkBalance(target.id); - await addOrSubtractBalance(target.id, add); + await addOrSubtractWallet(target.id, add); + displayBalance += add; } - //checks targets balance - targetBalance = await checkBalance(target.id); - if (!targetBalance && targetBalance !== 0) { + //checks if balance is valid + if (!startingBalance && startingBalance !== 0) { console.log(`ERROR: ${target.id}'s balance is NULL or undefined.`); await interaction.reply('An error has occured, please contact a developer') } const embed = new EmbedBuilder() .setAuthor({ name: `${target.displayName}'s Wallet`, iconURL: target.displayAvatarURL() }) - .addFields({ name: ' ', value: `Balance: ${targetBalance}` }); + .addFields({ name: ' ', value: `Balance: ${displayBalance}` }); //if no 'add' was specified, reply with embed of balance //otherwise, reply with the balance change, then send embed try { if (!add) { - await replyWithEmbed(embed, interaction); + await interaction.reply({ embeds: [embed] }); return; } - if (add && add > 0) { - await interaction.reply(`Added ${add} to ${target}'s wallet! (${startingBalance!} + ${add} = ${targetBalance})`); + if (add > 0) { + await interaction.reply(`Added ${add} to ${target}'s wallet! (${startingBalance} + ${add} = ${startingBalance + add})`); } - if (add && add < 0) { - await interaction.reply(`Removed ${add * -1} from ${target}'s wallet! (${startingBalance!} - ${add * -1} = ${targetBalance})`); + if (add < 0) { + await interaction.reply(`Removed ${add * -1} from ${target}'s wallet! (${startingBalance} - ${add * -1} = ${startingBalance + add})`); } await interaction.channel!.send({ embeds: [embed] }); } catch { - console.log(`ERROR: Failed to send reply and/or embed in ${interaction.channelId}`); + console.log(`ERROR: Could not send msg embed in ${interaction.channelId}`); return; } } diff --git a/src/commands/cat.ts b/src/commands/cat.ts index 1259785..3b2e455 100644 --- a/src/commands/cat.ts +++ b/src/commands/cat.ts @@ -1,12 +1,12 @@ import axios from 'axios'; -import { CommandInteraction, SlashCommandBuilder, EmbedBuilder } from 'discord.js'; +import { ChatInputCommandInteraction, SlashCommandBuilder, EmbedBuilder } from 'discord.js'; export const Cat = { info: new SlashCommandBuilder() .setName('cat') .setDescription('Sends a picture of a cat!'), - run: async (interaction: CommandInteraction): Promise => { + run: async (interaction: ChatInputCommandInteraction): Promise => { let catUrl: string; try { diff --git a/src/commands/dm.ts b/src/commands/dm.ts new file mode 100644 index 0000000..fd68321 --- /dev/null +++ b/src/commands/dm.ts @@ -0,0 +1,351 @@ +import { + ChatInputCommandInteraction, + SlashCommandBuilder, + EmbedBuilder, + ActionRowBuilder, + ButtonBuilder, + ComponentType, + MessageCollector, + User +} from 'discord.js'; +import { tryDelete, tryToDMEmbed } from '../utilities/miscUtils'; +import { BUTTONS } from '../constants/buttonConstants'; +import { TIME_IN_MS } from '../constants/miscConstants'; + +const MAX_CHOICES = 5; + +export const DM = { + info: new SlashCommandBuilder() + .setName('dm') + .setDescription('The bot will DM the specified user(s), a specified message.') + .addStringOption(option => + option.setName('message') + .setDescription('Please type the message you would like me to send!')) + .addBooleanOption(option => + option.setName('anon') + .setDescription('True: Name shown to recipients (default) | False: Name NOT shown.') + .setRequired(false)), + + addChoiceOptions: () => { + for (let i = 0; i < MAX_CHOICES; i++) { + DM.info.addUserOption(option => + option.setName(`user${i + 1}`) + .setDescription(`One of the users that will be DM\'d`) + .setRequired(false)); + } + }, + run: async (interaction: ChatInputCommandInteraction): Promise => { + if (!interaction.guild) { + await interaction.reply('This command can only be run in a server!'); + return; + } + const userID = interaction.user.id; + + let higherUp = false; + const getRole = interaction.guild.roles.cache; + const role = [ + getRole.find(role => role.name.toLowerCase() === 'moderator'), + getRole.find(role => role.name.toLowerCase() === 'officer'), + getRole.find(role => role.name.toLowerCase() === 'head raid leader') + ]; + const commandUser = await interaction.guild.members.fetch(userID); + for (const currentRole of role) { + if (!currentRole) { + console.log(`ERROR finding higher up role(s) for balance command`); + continue; + } + if (commandUser.roles.cache.has(currentRole.id)) { + higherUp = true; + break; + } + } + if (!higherUp) { + await interaction.reply('Only officer+ can use this command!'); + return; + } + + //checks the 'message' string option + const message = interaction.options.getString('message'); + let users: User[] = []; + //checks all 'user' options. + for (let i = 0; i < MAX_CHOICES; i++) { + const getUser = interaction.options.getUser(`user${i + 1}`); + if (getUser) { + users.push(getUser); + } + } + //if command users used the message and user options/shortcuts. + if (message && users.length > 0) { + //confirmation embed + const embed = new EmbedBuilder() + .setDescription(`**Are you sure you want to send this message to these users?**`) + .setFields( + { name: '__Message Recipients:__ ', value: users.join(', ') }, + { name: '__Current Message:__ ', value: message } + ); + + //confirmation action row with buttons + const row = new ActionRowBuilder() + .setComponents(BUTTONS.CANCEL_BUTTON, BUTTONS.YES_BUTTON); + + const embedMessage = await interaction.reply({ content: `Please read over the following:`, embeds: [embed], components: [row] }); + + const collector = embedMessage.createMessageComponentCollector({ + filter: i => i.user.id === interaction.user.id, + componentType: ComponentType.Button, + time: TIME_IN_MS.TEN_MINUTES + }); + + collector.on('collect', async i => { + + if (i.customId === BUTTONS.CANCEL_ID) { + await i.update({ components: [] }); + await interaction.editReply({ content: `Process Canceled.`, components: [] }); + return; + } + + else if (i.customId === BUTTONS.YES_ID) { + await i.update({ components: [] }); + await sendDMs(message, users); + return; + } + }) + } + //if there is a message and no users, or users and no message - specified options. + else if (message && users.length < 1 || !message && users.length > 0) { + await interaction.reply('If you specify a message, you must also specify at least one user (and vice-versa).'); + return; + } + //if no message or user options were specified + else { + const startEmbed = new EmbedBuilder() + .setTitle('DM User(s)') + .addFields( + { name: 'Please use the buttons below to: ', value: '- Create your desired message.' }, + { name: ' ', value: '- Choose which user(s) you would like the bot to send the message to.' }, + { name: ' ', value: '- Finally, click the "Send" button when you are finished.' } + ); + + const addMsgEmbed = new EmbedBuilder() + .setTitle('Create/Change Message') + .setDescription('Please type the message you would like me to send for you! Click "Done" when you are satisfied with the message.'); + + const addUsersEmbed = new EmbedBuilder() + .setTitle('Add Users') + .setDescription('Please type the names of the users you\'d like me to DM!'); + + const startRow = new ActionRowBuilder() + .setComponents(BUTTONS.CANCEL_BUTTON, BUTTONS.ADD_MESSAGE_BUTTON, BUTTONS.ADD_USERS_BUTTON, BUTTONS.SEND_BUTTON); + + const addMsgRow = new ActionRowBuilder() + .setComponents(BUTTONS.BACK_BUTTON, BUTTONS.DONE_BUTTON); + + const addUsersRow = new ActionRowBuilder() + .setComponents(BUTTONS.BACK_BUTTON, BUTTONS.DONE_BUTTON, BUTTONS.RESET_BUTTON); + + const confirmRow = new ActionRowBuilder() + .setComponents(BUTTONS.BACK_BUTTON, BUTTONS.YES_BUTTON); + + + const embedMessage = await interaction.reply({ content: 'Please follow the instructions below!', embeds: [startEmbed], components: [startRow] }); + + if (!embedMessage) { + await interaction.reply('An error has occurred, please contact a developer.'); + return; + } + + const buttonCollector = embedMessage.createMessageComponentCollector({ + filter: i => i.user.id === interaction.user.id, + componentType: ComponentType.Button, + time: TIME_IN_MS.TEN_MINUTES + }); + + let msgCollector: MessageCollector; + let userCollector: MessageCollector; + let memberList: User[] = []; + let DM = ''; + const msgTooLong = '**__Message too long! (limit: 1024 characters)__**'; + + buttonCollector.on('collect', async i => { + + if (i.customId === BUTTONS.ADD_MESSAGE_ID) { + await i.update({ embeds: [addMsgEmbed], components: [addMsgRow] }) + + msgCollector = interaction.channel!.createMessageCollector({ + filter: (m) => m.author.id === userID, + time: TIME_IN_MS.THIRTY_MINUTES, + }); + + msgCollector.on('collect', async m => { + await tryDelete(m); + if (m.content) { + DM = m.content; + if (DM.length > 1024) { + DM = msgTooLong; + } + addMsgEmbed.setFields({ name: '__Current Message:__ ', value: DM }); + } else { + await interaction.channel?.send('Could not read message.'); + } + await i.editReply({ embeds: [addMsgEmbed], components: [addMsgRow] }); + }); + } + + else if (i.customId === BUTTONS.ADD_USERS_ID) { + await i.update({ embeds: [addUsersEmbed], components: [addUsersRow] }); + + userCollector = interaction.channel!.createMessageCollector({ + filter: (m) => m.author.id === userID, + time: TIME_IN_MS.TEN_MINUTES, + }); + + userCollector.on('collect', async m => { + await tryDelete(m); + const words = m.content.split(' '); + + for (const currentWord of words) { + + let member = interaction.guild?.members.cache.find((member) => + member.user.username === currentWord || + member.user.displayName === currentWord || + member.user.id === currentWord + ); if (!member) { + const fetchMember = await interaction.guild?.members.fetch({ query: currentWord, limit: 1 }); + if (fetchMember) { + member = fetchMember.first(); + } + } + if (member) { + if (memberList.includes(member.user)) { + continue; + } else { + memberList.push(member.user); + } + } + addUsersEmbed.setFields({ name: '__Message Recipients:__ ', value: memberList.join(', ') }); + } + await interaction.editReply({ embeds: [addUsersEmbed], components: [addUsersRow] }); + }); + } + + else if (i.customId === BUTTONS.CANCEL_ID) { + await interaction.editReply({ content: 'Process canceled', components: [] }); + console.log('Stopped all Collectors'); + return; + } + + else if (i.customId === BUTTONS.DONE_ID) { + msgCollector.stop(); + userCollector.stop(); + + startEmbed.setTitle('DM User(s)') + .setFields( + { name: 'Please use the buttons below to: ', value: '- Create your desired message.' }, + { name: ' ', value: '- Choose which user(s) you would like the bot to send the message to.' }, + { name: ' ', value: '- Finally, click the "Send" button when you are finished.' } + ); + if (memberList.length > 0) { + startEmbed.addFields({ name: '__Message Recipients:__ ', value: memberList.join(', ') }); + } + if (DM.length > 0 && DM !== msgTooLong) { + startEmbed.addFields({ name: '__Current Message:__ ', value: DM }); + } + + await interaction.editReply({ embeds: [startEmbed], components: [startRow] }); + await i.update({}); + } + + else if (i.customId === BUTTONS.BACK_ID) { + msgCollector.stop(); + userCollector.stop(); + + await i.update({ embeds: [startEmbed], components: [startRow] }); + } + + else if (i.customId === BUTTONS.RESET_ID) { + memberList.length = 0; + addUsersEmbed.setDescription('Please type the names of the user(s) you\'d like me to DM!'); + addUsersEmbed.spliceFields(0, 1); + await i.update({ components: [addUsersRow] }); + await interaction.editReply({ embeds: [addUsersEmbed] }); + } + + else if (i.customId === BUTTONS.SEND_ID) { + if (!DM || memberList.length < 1) { + await interaction.channel?.send(`No message/no user specified!`); + await i.update({}) + } else { + startEmbed.setDescription(`**Are you sure you want to send this message to these users?**`); + startEmbed.setFields( + { name: '__Message Recipients:__ ', value: memberList.join(', ') }, + { name: '__Current Message:__ ', value: DM } + ); + await interaction.editReply({ embeds: [startEmbed], components: [confirmRow] }); + await i.update({ components: [confirmRow] }); + } + } else if (i.customId === BUTTONS.YES_ID) { + buttonCollector.stop(); + await i.update({}) + await sendDMs(DM, memberList); + } + }); + } + /** + * function specifically for this command. + * Attempts to send specified message to all specified users. + * + * @param {string} m - The message that is to be sent to all users + * @param {User[]} users - Object array of all specified discord users. + */ + async function sendDMs(m: string, users: User[]): Promise { + const DMEmbed = new EmbedBuilder() + .addFields( + { name: '__Message Content:__ ', value: m }, + { name: ' ', value: '--END--' }, + { + name: ' ', value: `** **\n*__Note:__ This bot cannot recieve DMs. ` + + `For any questions or concerns regarding this message, please contact a higher up in the related Discord server.*\n** **` + }, + ) + .setColor('#8B0000'); + //tries to get the server icon + const serverIcon = interaction.guild?.iconURL(); + if (serverIcon) { + DMEmbed.setAuthor({ name: interaction.guild!.name, iconURL: serverIcon }) + } else { + DMEmbed.setAuthor({ name: interaction.guild!.name }); + } + //if the command user wants to be anonymous or not (name will be shown if false) + const anon = interaction.options.getBoolean('anon'); + if (!anon) { + const userIcon = interaction.user.avatarURL(); + if (userIcon) { + DMEmbed.setFooter({ text: `Message sent by: ${interaction.user.displayName} (${interaction.user.username})`, iconURL: userIcon }); + } else { + DMEmbed.setFooter({ text: `Message sent by: ${interaction.user.displayName} (${interaction.user.username})` }); + } + } + //stores all users that were successfully DM'd + let sentArr: User[] = []; + //stores all users that could not be DM'd + let notSentArr: User[] = []; + for (const currentMember of users) { + const sent = await tryToDMEmbed(DMEmbed, currentMember); + if (sent) { + sentArr.push(currentMember); + } else if (!sent) { + notSentArr.push(currentMember); + } + } + if (sentArr.length > 0) { + await interaction.channel?.send(`Successfully DM'd: ${sentArr.join(', ')}`); + } + if (notSentArr.length > 0) { + await interaction.channel?.send(`Failed to DM: ${notSentArr.join(', ')}`); + } + await interaction.editReply({ content: 'Sent!', embeds: [DMEmbed], components: [] }); + return; + } + + } +}; diff --git a/src/commands/mock.ts b/src/commands/mock.ts index 7a9fdb1..7889662 100644 --- a/src/commands/mock.ts +++ b/src/commands/mock.ts @@ -1,4 +1,4 @@ -import { CommandInteraction, Message, SlashCommandBuilder } from 'discord.js'; +import { ChatInputCommandInteraction, Message, SlashCommandBuilder } from 'discord.js'; import { mockTargets } from '../index'; export const Mock = { @@ -10,7 +10,7 @@ export const Mock = { .setDescription('Choose which member you want to be mocked') .setRequired(true)), - run: async (interaction: CommandInteraction): Promise => { + run: async (interaction: ChatInputCommandInteraction): Promise => { if (!interaction.guild) { //Checks if the command is NOT done in a server await interaction.reply('This command can only be done in a server.'); return; @@ -47,6 +47,7 @@ export const Mock = { } mockTargets.add(target.id); await interaction.reply(`Sure thing, I will begin mocking ${target}!`); + console.log(`Current Mock list: ${mockTargets}`); }, /** * Deletes 'mock' targets message and re-sends it in "sPoNgEbOb" text format @@ -69,7 +70,6 @@ export const Mock = { shouldBeLower = !shouldBeLower; } } - // Attempt to delete the original message try { await message.delete(); @@ -78,7 +78,6 @@ export const Mock = { console.log(`ERROR: could not delete ${message.author.username} (${message.author.id}) message in ${message.channel}`); return; } - // Attempt to send the mocked message try { await message.channel.send(`${message.author} says "${mockMessage}"`); diff --git a/src/commands/ping.ts b/src/commands/ping.ts index 1832b4e..86f9330 100644 --- a/src/commands/ping.ts +++ b/src/commands/ping.ts @@ -1,11 +1,11 @@ -import { CommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js'; export const Ping = { info: new SlashCommandBuilder() .setName('ping') .setDescription('Tells you the bot\'s ping (ms)'), - run: async (interaction: CommandInteraction): Promise => { + run: async (interaction: ChatInputCommandInteraction): Promise => { await interaction.reply(`${interaction.client.ws.ping}ms`); } }; diff --git a/src/commands/poll.ts b/src/commands/poll.ts index 56dd91c..5f4bcb9 100644 --- a/src/commands/poll.ts +++ b/src/commands/poll.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, EmbedBuilder, SlashCommandBuilder } from 'discord.js'; -import { NUMBER_EMOJIS } from '../utils'; +import { NUMBER_EMOJIS } from '../constants/miscConstants'; const MAX_CHOICES = 5; @@ -27,15 +27,15 @@ export const Poll = { } const questionString = interaction.options.getString('question'); - if (!questionString || typeof questionString !== 'string') return; + if (!questionString) return; //char limit if (questionString.length > 256) { - interaction.reply(`Question is too long. (max 256 char.)`); + await interaction.reply(`Question is too long. (max 256 char.)`); } const choices = []; - for (let i = 1; i <= 5; i++) { + for (let i = 1; i <= MAX_CHOICES; i++) { const choice = interaction.options.get(`choice${i}`)?.value; if (choice) { choices.push(choice); @@ -53,18 +53,22 @@ export const Poll = { name: interaction.user.displayName, iconURL: interaction.user.displayAvatarURL() }) - //Discord embed spacing to look cleaner + + //Discord embed spacing to look nicer .addFields( - { name: ' ', value: '** **' } + { name: ' ', value: '** **' }, + { name: ' ', value: '** **' }, ); for (let i = 0; i < choices.length; i++) { embed.addFields( { name: ' ', value: `${NUMBER_EMOJIS[i]} ${choices[i]}` }, ) if (i + 1 < choices.length) { + //more spacing //more spacing embed.addFields( { name: ' ', value: '** **' }, + { name: ' ', value: '** **' }, ) } }; @@ -83,7 +87,7 @@ export const Poll = { } } } catch { - console.log(`ERROR: Could not send embed in ${interaction.channelId}`); + console.log(`ERROR: Could not send embed in ${interaction.channelId})`); } } diff --git a/src/commands/roll.ts b/src/commands/roll.ts index 97bbaee..1a5fe0e 100644 --- a/src/commands/roll.ts +++ b/src/commands/roll.ts @@ -1,119 +1,186 @@ -import { JeffreyGachaURLs, displayLegendary } from "../JeffreyGacha"; -import { SlashCommandBuilder, CommandInteraction, EmbedBuilder } from "discord.js"; -import { replyWithEmbed, rng } from '../utils'; import { - addNewGacha, - findOrAddUserWallet, - addOrSubtractBalance, - checkBalance, - gachaLvlUp, + SlashCommandBuilder, + EmbedBuilder, + ChatInputCommandInteraction, + ButtonBuilder, + ActionRowBuilder, + ComponentType, + Message +} from "discord.js"; +import { + addOrSubtractWallet, + addToCollection, checkGachaLevel, - checkIfUserHasGachaInv -} from "../DBUtils"; + checkOrStartWallet +} from "../databse/DBMain"; +import { rollForGacha } from "../databse/DBUtils"; +import { checkIfFirstOrLast, getEmbedColor, tryDelete } from "../utilities/miscUtils"; +import { BUTTONS } from "../constants/buttonConstants"; + +let rollAgainInUse = false; export const Roll = { info: new SlashCommandBuilder() .setName('roll') - .setDescription('Roll for Jeffrey\'s!'), + .setDescription('Roll for Jeffrey\'s!') + .addIntegerOption(option => + option.setName('number') + .setDescription('How many rolls you want to do (costs 10 coins each!)') + .setRequired(false)), - run: async (interaction: CommandInteraction): Promise => { + run: async (interaction: ChatInputCommandInteraction, rollingAgain?: number): Promise => { if (!interaction.guild) { await interaction.reply('this command can only be done in a server'); return; } - const userID = interaction.user.id; - - await findOrAddUserWallet(userID); - - let currentBalance = await checkBalance(userID); - - if (!currentBalance && currentBalance !== 0) { - console.log(`${userID}'s currentBalance is NULL or undefined`); - interaction.reply('Failed to check balance, please contact a developer'); - return; + let rolls = 1 + if (rollingAgain && rollingAgain > 0) { + rolls = rollingAgain; + } else { + const checkIfRolls = interaction.options.getInteger('number') + if (checkIfRolls) { + rolls = checkIfRolls; + } } - const price: number = 5; - - if (currentBalance < price) { - await interaction.reply('not enough JeffreyCoins!'); + const userID = interaction.user.id; + const currentWallet = await checkOrStartWallet(userID); + + const price = 10; + const totalPrice = price * rolls; + if (currentWallet < totalPrice) { + if (rollingAgain) { + await interaction.channel?.send('Not enough JeffreyCoins!'); + return; + } + await interaction.reply('Not enough JeffreyCoins!'); return; } else { - await addOrSubtractBalance(userID, -price); - currentBalance -= price; + await addOrSubtractWallet(userID, -totalPrice); } + const gacha = await rollForGacha(rolls); + + let gachaLevel: number[] = []; + let level: string[] = []; + const embeds: EmbedBuilder[] = []; + + //Cycle through all rolls, creates embed for each gacha acquired + for (let i = 0; i < rolls; i++) { + await addToCollection(userID, gacha[i].id); + const getLevel = await checkGachaLevel(userID, gacha[i].id); + if (!getLevel) { + console.log(`ERROR: Invalid level for - User: ${userID}, Gacha: ${gacha[i].id}`); + await interaction.reply('An error has occured, please contact a developer'); + } + gachaLevel.push(getLevel); - const rndm = await rng(0, 100); - let raritySelect: 'Common' | 'Uncommon' | 'Rare' | 'Legendary'; - - if (rndm <= 65) { - raritySelect = 'Common'; - } else if (rndm > 65 && rndm <= 90) { - raritySelect = 'Uncommon'; - } else if (rndm > 90 && rndm < 99) { - raritySelect = 'Rare'; - } else if (rndm >= 100) { - raritySelect = 'Legendary'; - } else { - console.log('error choosing random rarity for Roll command'); - return; + let starArray = ''; + for (let j = 0; j < gachaLevel[i]; j++) { + starArray += '⭐'; + } + let lvlUp = 'Level: '; + if (gachaLevel[i] > 1) { + lvlUp = '**Rank Up!** | Level: '; + } + level.push(lvlUp + starArray); + + const color = await getEmbedColor(gacha[i].rarity); + const embed = new EmbedBuilder(); + embed.setTitle(`${gacha[i].name} Jeffrey`) + .setDescription(`${gacha[i].description}`) + .setImage(gacha[i].link) + .setFields({ name: ' ', value: `${level[i]}` }) + .setColor(color) + .setFooter({ text: `${0 + i + 1}/${rolls}` }); + + embeds.push(embed); } - const rarity = JeffreyGachaURLs[raritySelect]; - const chooseGacha = await rng(0, rarity.length - 1); - const gachaObj = rarity[chooseGacha]; - const gacha = gachaObj.link; + if (rolls === 1) { + BUTTONS.NEXT_BUTTON.setDisabled(true); + } + //previous starts disabled + const row = new ActionRowBuilder() + .setComponents(BUTTONS.PREVIOUS_BUTTON, BUTTONS.NEXT_BUTTON, BUTTONS.ROLL_AGAIN_BUTTON); - const displayRarity = raritySelect.toUpperCase(); + let currentGacha = 0; - const gachaInv = await checkIfUserHasGachaInv(userID, gacha); - if (!gachaInv) { - console.log(gacha); - await addNewGacha(userID, gacha); + if (rollingAgain && rollingAgain > 0) { + await interaction.channel?.send(`${interaction.user} rolled for ${rolls} Jeffrey(s)!`); } else { - await gachaLvlUp(userID, gacha); + await interaction.reply(`${interaction.user} rolled for ${rolls} Jeffrey(s)!`); } - let embed: EmbedBuilder; - - if (raritySelect !== 'Legendary') { - embed = new EmbedBuilder() - .setTitle(`You pulled a **${displayRarity}** Jeffrey!`) - .setAuthor({ name: `${interaction.user.displayName}` }) - .setImage(gacha); + const gachaMessage = await interaction.channel?.send({ + content: `**${gacha[currentGacha].rarity.toUpperCase()} JEFFREY**`, + embeds: [embeds[currentGacha]], + components: [row] + }); - } else { - const legendaryInfo = await displayLegendary(gacha); + if (!gachaMessage) { + await interaction.reply('Could not send message embed, please contact a developer'); + return; + } - if (!legendaryInfo || !legendaryInfo[0] || !legendaryInfo[1]) { - console.log(`ERROR: could not find legendaryInfo`); - await interaction.reply('Sorry the command could not be completed, please contact a developer.'); - return; - } else { - embed = new EmbedBuilder() - .setTitle('YOU PULLED A LEGENDARY JEFFREY!!!') - .setAuthor({ name: `${interaction.user.displayName}` }) - .setImage(gacha) - .addFields({ name: `${legendaryInfo[0]}`, value: `${legendaryInfo[1]}` }); + const buttonCollector = gachaMessage.createMessageComponentCollector({ filter: i => i.user.id === interaction.user.id, componentType: ComponentType.Button, time: 1_800_000 }); + + buttonCollector.on('collect', async i => { + + if (i.customId === BUTTONS.NEXT_ID) { + currentGacha += 1; + await checkIfFirstOrLast(currentGacha, rolls); + await i.update({ + content: `**${gacha[currentGacha].rarity.toUpperCase()}**`, + embeds: [embeds[currentGacha]], + components: [row] + }); + } + + if (i.customId === BUTTONS.PREVIOUS_ID) { + currentGacha -= 1; + await checkIfFirstOrLast(currentGacha, rolls); + await i.update({ + content: `**${gacha[currentGacha].rarity.toUpperCase()}**`, + embeds: [embeds[currentGacha]], + components: [row] + }); } - } - const gachaLevel = await checkGachaLevel(userID, gacha); - let starArray = ''; - if (!gachaLevel) { - console.log(`gachaLevel is ${gachaLevel} (NULL)`); - await interaction.reply('Sorry the command could not be completed, please contact a developer.') - } - for (let i = 0; i < gachaLevel; i++) { - starArray += '⭐'; - } - embed.setDescription(starArray); - try { - await replyWithEmbed(embed!, interaction); - } catch { - console.log(`ERROR: could not reply with embed in ${interaction.channelId}`); - return; - } + if (i.customId === BUTTONS.ROLL_AGAIN_ID) { + if (rollAgainInUse) { + await i.update({}) + await interaction.channel?.send('Please complete your previous roll again before using this one!'); + } else { + rollAgainInUse = true; + row.setComponents(BUTTONS.PREVIOUS_BUTTON, BUTTONS.NEXT_BUTTON); + + await i.update({ components: [row] }); + const rollAgainMsg = await interaction.channel?.send(`How many more rolls would you like to do? (Please type a number, type '0' to cancel)`); + + const msgFilter = (m: Message) => m.author.id === interaction.user.id; + const msgCollector = interaction.channel!.createMessageCollector({ filter: msgFilter, time: 60_000 }); + + msgCollector.on('collect', async m => { + await tryDelete(m); + if (m.content === '0') { + await tryDelete(rollAgainMsg!); + msgCollector.stop(); + } + + const content = m.content; + const parsedNumber = Number(content); + if (!isNaN(parsedNumber)) { + msgCollector.stop(); + rollAgainInUse = false; + await Roll.run(interaction, parsedNumber); + return; + } else { + await interaction.channel?.send('Please type a valid number!'); + } + }); + } + } + }); } }; \ No newline at end of file diff --git a/src/constants/buttonConstants.ts b/src/constants/buttonConstants.ts new file mode 100644 index 0000000..b6e1817 --- /dev/null +++ b/src/constants/buttonConstants.ts @@ -0,0 +1,70 @@ +import { ButtonBuilder, ButtonStyle } from "discord.js" + +export namespace BUTTONS { + export const NEXT_ID: string = 'next'; + export const NEXT_BUTTON = new ButtonBuilder() + .setCustomId(NEXT_ID) + .setLabel('Next ➡') + .setStyle(ButtonStyle.Primary); + + export const PREVIOUS_ID: string = 'previous'; + export const PREVIOUS_BUTTON = new ButtonBuilder() + .setCustomId(PREVIOUS_ID) + .setLabel('⬅ Previous') + .setStyle(ButtonStyle.Primary) + .setDisabled(true); + + export const ROLL_AGAIN_ID: string = 'roll_again'; + export const ROLL_AGAIN_BUTTON = new ButtonBuilder() + .setCustomId(ROLL_AGAIN_ID) + .setLabel('🔁 Roll Again!') + .setStyle(ButtonStyle.Danger); + + export const ADD_MESSAGE_ID: string = 'add_message'; + export const ADD_MESSAGE_BUTTON = new ButtonBuilder() + .setCustomId('add_message') + .setLabel('✉ Add/Change Message') + .setStyle(ButtonStyle.Primary); + + export const ADD_USERS_ID: string = 'add_users'; + export const ADD_USERS_BUTTON = new ButtonBuilder() + .setCustomId('add_users') + .setLabel('➕ Add Users') + .setStyle(ButtonStyle.Primary); + + export const SEND_ID: string = 'send'; + export const SEND_BUTTON = new ButtonBuilder() + .setCustomId('send') + .setLabel('📤 Send') + .setStyle(ButtonStyle.Success); + + export const BACK_ID: string = 'back'; + export const BACK_BUTTON = new ButtonBuilder() + .setCustomId('back') + .setLabel('⬅ Back') + .setStyle(ButtonStyle.Danger); + + export const DONE_ID: string = 'done'; + export const DONE_BUTTON = new ButtonBuilder() + .setCustomId('done') + .setLabel('✅ Done') + .setStyle(ButtonStyle.Success); + + export const RESET_ID: string = 'reset'; + export const RESET_BUTTON = new ButtonBuilder() + .setCustomId('reset') + .setLabel('🔃 Reset') + .setStyle(ButtonStyle.Danger); + + export const CANCEL_ID: string = 'cancel'; + export const CANCEL_BUTTON = new ButtonBuilder() + .setCustomId('cancel') + .setLabel('❌ Cancel') + .setStyle(ButtonStyle.Danger); + + export const YES_ID: string = 'yes'; + export const YES_BUTTON = new ButtonBuilder() + .setCustomId('yes') + .setLabel('✅ Yes') + .setStyle(ButtonStyle.Success); +} \ No newline at end of file diff --git a/src/constants/miscConstants.ts b/src/constants/miscConstants.ts new file mode 100644 index 0000000..a9e9bb8 --- /dev/null +++ b/src/constants/miscConstants.ts @@ -0,0 +1,19 @@ +export const NUMBER_EMOJIS: string[] = [ + "1⃣", + "2⃣", + "3⃣", + "4⃣", + "5⃣", + "6⃣", + "7⃣", + "8⃣", + "9⃣", + "🔟" +]; + +export const TIME_IN_MS = { + ONE_MINUTE: 60_000, + TEN_MINUTES: 600_000, + THIRTY_MINUTES: 1_800_000, + ONE_HOUR: 3_600_000 +}; \ No newline at end of file diff --git a/src/databse/DBMain.ts b/src/databse/DBMain.ts new file mode 100644 index 0000000..410eca3 --- /dev/null +++ b/src/databse/DBMain.ts @@ -0,0 +1,134 @@ +import { User, Wallet, Gacha, Collection } from "./JeffreyDB"; + +const LEVEL_CAP = 5; +/** + * Checks if the given userID is in the "User" table, if not add it. + * + * @param {string} userID - Discord ID + * @param {string} username - Discord Username + * @param {string} displayName - Discord users Display Name in that server + * @returns {Promise} - Returns 'User' object which contains all information from the found/created row in table. + */ +export async function findOrAddToUser(userID: string, username: string, displayName: string): Promise { + const [user, created] = await User.findOrCreate({ where: { id: userID, name: username, display_name: displayName } }); + if (created) { + console.log(`Created new "User" row for - User: ${userID}`); + } else { + console.log(`User already in database "User" - User: ${userID}`); + } + return user; +}; + +/** + * Searches for the 'balance' associated the given user ID. + * If none was found, creates one. + * The user ID is the same for that user in every database table that contains a user_id. + * + * @param {string} userID - Discord ID + * @returns {Promise} - Returns the users current balance in "Wallet" table (defaults to 0 if Wallet was created) + */ +export async function checkOrStartWallet(userID: string): Promise { + const [userWallet, created] = await Wallet.findOrCreate({ where: { user_id: userID } }); + if (created) { + console.log(`Created new "Wallet" row for - User: ${userID}`); + return 0; + } else { + console.log(`User already in database "Wallet" - User: ${userID}`); + } + console.log(`User: ${userID} has a balance of ${userWallet.balance}`); + return userWallet.balance; +}; + +/** + * Add or Subtract the 'balance' associated with the user ID in the "Wallet" table by 'amount'. + * Function should only be used if you know that the row with 'user_id' exists (ie: call the checkOrStartWallet) + * + * @param {string} userID - Discord ID + * @param {number} amount - Positive or Negative integer to Add or Subtract 'balance' by + */ +export async function addOrSubtractWallet(userID: string, amount: number): Promise { + await Wallet.increment({ balance: amount }, { where: { user_id: userID } }); + console.log(`${userID}'s wallet has been changed by: ${amount}`); +}; + +/** + * Checks for a row in GachaInv that contains both 'userID' and 'gachaID'. + * Looking for if the user has previously aquired this gacha.(ie: if its a duplicate) + * If its a duplicate, increase its level by 1 (up to level cap of 5); + * + * @param {string} userID - Discord ID + * @param {number} gachaID - ID associated with specific gacha + * @returns {Promise} - 'Collection' object contains all information in affected row. + */ +export async function levelUpOrAddGachaToCollection(userID: string, gachaID: number): Promise { + const [gacha, created] = await Collection.findOrCreate({ where: { user_id: userID, gacha_id: gachaID } }); + if (!created) { + if (gacha.level < LEVEL_CAP) { + const [leveledGacha] = await Collection.increment({ level: 1 }, { where: { user_id: userID, gacha_id: gachaID } }); + gacha.level += 1; + return gacha; + } else { + return gacha; + } + } else { + console.log(`${gachaID} added to Collection for - User: ${userID}`); + return gacha; + } +}; + +/** + * Checks the 'level' column that is in the same row as userID and gachaID in 'Collection' table. + * Should only be used when you know that gachaID exists in the same row as userID + * + * @param {string} userID - Discord ID + * @param {number} gachaID - ID associated with specific gacha + * @returns {Promise} - The 'level' column in the row containing 'userID' and 'gachaID' + */ +export async function checkGachaLevel(userID: string, gachaID: number): Promise { + const lvl = await Collection.findOne({ where: { user_id: userID, gacha_id: gachaID } }); + if (lvl) { + return lvl.level; + } else { + return 0; + } +}; +/** + * Get the link, name, description and rarity from a specific Gacha. + * + * @param {number} gachaID - Gacha ID associated with a specifc Gacha. + * @returns {Promise} - returns the 'Gacha' object which contains information about a specifc gacha + */ +export async function gachaInfo(gachaID: number): Promise { + const gachaInfo = await Gacha.findOne({ where: { id: gachaID } }); + if (gachaInfo) { + return gachaInfo; + } else { + return null; + } +}; +/** + * Checks if a user has a specific gacha, if they do, increase its level by one, + * otherwise, create a new row with that gacha. + * + * @param {string} userID - Users Discord ID + * @param {number} gachaID - ID associated with a specific Gacha + * @returns {Promise} - 'Collection' object containing information on specified row in table. + * - Also returns true if Gacha is max level. + */ +export async function addToCollection(userID: string, gachaID: number): Promise { + const gacha = await Gacha.findOne({ where: { id: gachaID } }) + const [userGacha, created] = await Collection.findOrCreate({ where: { user_id: userID, gacha_id: gacha!.id } }); + if (!created) { + if (userGacha.level < LEVEL_CAP) { + await userGacha.increment({ level: 1 }); + await userGacha.reload(); + console.log(`Increased level of - GachaID: ${gachaID} for - User: ${userID} `); + return userGacha; + } else { + return [userGacha, true]; + } + } else { + console.log(`GachaID: ${gachaID} - has been added to the Collection of - User: ${userID}`); + return userGacha + } +}; diff --git a/src/databse/DBUtils.ts b/src/databse/DBUtils.ts new file mode 100644 index 0000000..6bbd661 --- /dev/null +++ b/src/databse/DBUtils.ts @@ -0,0 +1,51 @@ +import { Gacha } from './JeffreyDB'; +import { rng } from "../utilities/miscUtils"; + +/** + * Takes a rarity, randomly picks one gacha among that rarity, returns it + * + * @param {string} rarity - Which rarity it should choose from + * @returns {Promise} - random 'rarity' gacha + */ +export async function getRandomGachaOfRarity(rarity: string): Promise { + const allGachaOfRarity = await Gacha.findAll({ where: { rarity: rarity } }); + const randomGacha = Math.floor(Math.random() * Math.floor(allGachaOfRarity.length)); + return allGachaOfRarity[randomGacha]; +} + +/** + * Calls getRandomGachaOfRarity and specifies that it wants a 'legendary' gacha + * + * @returns {Promise} - 'Gacha' object containing table information about a specific Gacha + */ +export async function getRandomLegendary(): Promise { + const legendaryGacha = await getRandomGachaOfRarity('legendary'); + return legendaryGacha; +} + +/** + * Randomly selects rarity, then calls getRandomGachaOfRarity to choose a random gacha of that rarity + * + * @param {number} rolls - The number of gacha that should be returned + * @returns {Promise} - 'Gacha' object containing table information about a random gacha of random rarity. + */ +export async function rollForGacha(rolls: number): Promise { + const gacha: Gacha[] = []; + for (let i = 0; i < rolls; i++) { + const rndm = await rng(0, 100); + let rarity: string; + if (rndm <= 65) { + rarity = 'common'; + } else if (rndm > 65 && rndm <= 90) { + rarity = 'uncommon'; + } else if (rndm > 90 && rndm <= 99) { + rarity = 'rare'; + } else if (rndm >= 100) { + rarity = 'legendary'; + } + const randomGacha = await getRandomGachaOfRarity(rarity!); + gacha.push(randomGacha); + } + + return gacha; +} \ No newline at end of file diff --git a/src/databse/JeffreyDB.ts b/src/databse/JeffreyDB.ts new file mode 100644 index 0000000..d215cf7 --- /dev/null +++ b/src/databse/JeffreyDB.ts @@ -0,0 +1,197 @@ +import { Sequelize, DataTypes, Model } from 'sequelize'; + +export const sequelize = new Sequelize({ + dialect: 'sqlite', + storage: './JeffreyDB.db', + logging: false, +}); + +/** Creates the "User" table with: +* @example (id is Discord user ID, name is Discord Username, display_name is their nickname in that server) +* | id:'667951424704872450' | name: 'jeffreyhassalide' | display_name: Jeffrey | +*/ +export class User extends Model { + declare id: string; + declare name: string; + declare display_name: string; +} +User.init({ + id: { + type: DataTypes.STRING, + allowNull: false, + primaryKey: true, + unique: true, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, + display_name: { + type: DataTypes.STRING, + allowNull: false, + }, +}, { sequelize }); + +/** Creates the "Wallet" table with +* @example(id is a unique ID for each row, user_id is users Discord ID, balance is how much money they have) +* | id: 5 | user_id: 667951424704872450 | balance: 10 | +*/ +export class Wallet extends Model { + declare id: number; + declare user_id: string; + declare balance: number; +} +Wallet.init({ + id: { + type: DataTypes.INTEGER, + primaryKey: true, + allowNull: false, + autoIncrement: true, + }, + user_id: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + references: { + model: User, + key: 'id', + }, + }, + balance: { + type: DataTypes.INTEGER, + allowNull: false, + unique: false, + defaultValue: 0, + } +}, { sequelize }); +User.hasOne(Wallet, { foreignKey: 'user_id', sourceKey: 'id' }); + +/** Creates the "Gacha" table which contains a link, name, description and rarity for each gacha, aswell as a unique number ID for each of them. +* @example +* | id: 8 | link: https://fakelink.png | name: JeffreyDaKilla | description: He gonna scratch you up! | rarity: legendary | +*/ +export class Gacha extends Model { + declare id: number; + declare link: string; + declare name: string; + declare description: string; + declare rarity: string; +} + +Gacha.init({ + id: { + type: DataTypes.INTEGER, + primaryKey: true, + allowNull: false, + unique: true, + autoIncrement: true, + }, + link: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, + description: { + type: DataTypes.STRING, + allowNull: false, + }, + rarity: { + type: DataTypes.STRING, + allowNull: false, + unique: false, + }, +}, { sequelize }); + +/** + * Creats 'Collection' table which creates rows with user_id and gacha_id, to show which gachas each user owns, + * as well as what level each specific gacha is for that user(level = how many copies they have, caps at 5) + * Has a unique number ID for each row. + * @example + * | id: 25 | user_id: 667951424704872450 | gacha_id: 48 | level: 3 | + */ +export class Collection extends Model { + declare id: number; + declare user_id: string; + declare gacha_id: number; + declare level: number; +} +Collection.init({ + id: { + type: DataTypes.INTEGER, + primaryKey: true, + allowNull: false, + unique: true, + autoIncrement: true, + }, + user_id: { + type: DataTypes.STRING, + allowNull: false, + unique: false, + references: { + model: User, + key: 'id', + }, + }, + gacha_id: { + type: DataTypes.INTEGER, + allowNull: false, + unique: false, + references: { + model: Gacha, + key: 'id', + }, + }, + level: { + type: DataTypes.INTEGER, + allowNull: false, + unique: false, + defaultValue: 1, + }, +}, { sequelize }); +User.hasMany(Collection, { foreignKey: 'user_id', sourceKey: 'id' }); +Gacha.hasMany(Collection, { foreignKey: 'gacha_id', sourceKey: 'id' }); + +// Contains'test' and 'sync' +export const DB = { + /** + * test: + * Checks if it has access to the database file + */ + test: async (): Promise => { + try { + await sequelize.authenticate(); + console.log('Connection has been established successfully.'); + } catch (error) { + console.error('Unable to connect to the database:', error); + } + }, + /** + * sync: + * Tries to sync all database tables with the database file, + * which allows it to build upon the information previously stored in their respective tables after bot resets. + */ + sync: async (): Promise => { + try { + await User.sync(); + console.log('UsersDB synced'); + + await Wallet.sync(); + console.log('UserWallets synced'); + + await Gacha.sync(); + console.log('GachaInvs synced'); + + await Collection.sync() + console.log('Collection sunced'); + + } catch (err) { + console.log(); + console.error('Failed to sync table(s)', err); + } + } +}; \ No newline at end of file diff --git a/src/databse/JeffreyGacha.ts b/src/databse/JeffreyGacha.ts new file mode 100644 index 0000000..cc7d9b9 --- /dev/null +++ b/src/databse/JeffreyGacha.ts @@ -0,0 +1,90 @@ +//Object containing the 4 rarities, 'Common', 'Uncommon', 'Rare' and 'Legendary'. +//Each rarity is an array of gacha objects, containing the link(URL), name and description of all gachas. + +export const GACHA_URL_LIST = [ + { link: 'https://imgur.com/Oyh1TiC.png', name: 'Prepared', description: 'He\'s comfy, but not *too* comfy...', rarity: 'common' }, + { link: 'https://imgur.com/hr1tx64.png', name: 'Soul Stare', description: 'Those tiny pupil\'s can see your true intentions.', rarity: 'common' }, + { link: 'https://imgur.com/xQqwrWD.png', name: 'Blue Blanket', description: 'He prefers the softer, cozier douvet, but grandma\'s quilt will do.', rarity: 'common' }, + { link: 'https://imgur.com/4mrEWuV.png', name: 'You Can Leave Now', description: '\"Let me get some rest, Human!\"', rarity: 'common' }, + { link: 'https://imgur.com/iSHcsOd.png', name: 'Doesn\'t Notice', description: 'This silly billy doesn\'t even know we\'re here!', rarity: 'common' }, + { link: 'https://imgur.com/cSprAX4.png', name: 'Tail-Pillow', description: 'A multi-purpose apendage! (let\'s pretend he isn\'t using his paws)', rarity: 'common' }, + { link: 'https://imgur.com/DaCVzq8.png', name: 'Skyblock', description: 'He wants to try!', rarity: 'common' }, + { link: 'https://imgur.com/Z2LaGwY.png', name: 'Sweet', description: 'Get it? Cuz of the sugarcane? Yeah, im not getting lazy with these, im just clever.', rarity: 'common' }, + { link: 'https://imgur.com/mx70oY2.png', name: 'Are You Even Listening', description: '\"FOCUS ON ME HUMAN\"', rarity: 'common' }, + { link: 'https://imgur.com/AivmZ0T.png', name: 'Bed-Head', description: 'Man, he looks rough.', rarity: 'common' }, + { link: 'https://imgur.com/6dD6wHv.png', name: 'Daring', description: 'He made the jump (probably)', rarity: 'common' }, + { link: 'https://imgur.com/sqnH5tg.png', name: 'Strategic', description: 'He could build a much better empire.', rarity: 'common' }, + { link: 'https://imgur.com/nXo9YbZ.png', name: 'Living On The Edge', description: 'He does this way too often.', rarity: 'common' }, + { link: 'https://imgur.com/po28ZPo.png', name: 'Attack On', description: 'The green thing is an AOT cloak (it\'s *very* soft).', rarity: 'common' }, + { link: 'https://imgur.com/zEfh9G1.png', name: 'Laundry', description: 'Silly Jeffrey, you aren\'t clothes!', rarity: 'common' }, + { link: 'https://imgur.com/MsftbNA.png', name: 'Red Blanket', description: 'That\'s won tiewd wittuwl meow meow.', rarity: 'common' }, + { link: 'https://imgur.com/GJFRw0N.png', name: 'Spot-Stealing', description: '\"Oh, was someone sitting here?\"', rarity: 'common' }, + { link: 'https://imgur.com/Qgi4yeQ.png', name: 'Desk Bed', description: 'The first picture of Jeffrey on the desk-bed! (at least, while its on the desk)', rarity: 'common' }, + { link: 'https://imgur.com/bpVQ6xz.png', name: 'Coat', description: 'My sister wasn\'t too happy when I showed her this one.', rarity: 'common' }, + { link: 'https://imgur.com/DdhzNO2.png', name: 'TV-Time', description: 'His favourite position for watching TV with us! (The tv is directly behind him...)', rarity: 'common' }, + { link: 'https://imgur.com/AvqpwfX.png', name: 'Severed Head', description: 'At least he still has his best feature\'s!', rarity: 'common' }, + { link: 'https://imgur.com/RuOtxcv.png', name: 'Dog Bed', description: 'Wrong bed silly, that one\'s way too big!', rarity: 'common' }, + { link: 'https://imgur.com/6WkntdW.png', name: 'Morning Selfie', description: 'Not his best look.', rarity: 'common' }, + { link: 'https://imgur.com/40M1KOh.png', name: 'Lean-Back', description: 'What a strange sleeping positon.', rarity: 'common' }, + { link: 'https://imgur.com/ZtPZZV9.png', name: 'Itchy-Ear', description: 'They say this is the only picture in existence of Jeffrey scratching his ear!', rarity: 'common' }, + + { link: 'https://imgur.com/txgkO4S.png', name: 'Shower', description: 'Would be a shame if I were to...', rarity: 'uncommon' }, + { link: 'https://imgur.com/gqbRfhW.png', name: 'Cats Only', description: '\"No Humans allowed.\"', rarity: 'uncommon' }, + { link: 'https://imgur.com/qYIxVan.png', name: 'Bunker', description: 'With his perfect defense, he\'s ready to attack!', rarity: 'uncommon' }, + { link: 'https://imgur.com/dord5jv.png', name: 'Sunglass', description: '\"Hey Human, can you buy me some sunglasses?\"', rarity: 'uncommon' }, + { link: 'https://imgur.com/WGDO515.png', name: 'Superior', description: '\"I am higher than you, so I am better than you.\"', rarity: 'uncommon' }, + { link: 'https://imgur.com/6ur3hgK.png', name: 'Photo Pose', description: 'Does he think he\'s in a photoshoot or something?', rarity: 'uncommon' }, + { link: 'https://imgur.com/J1Ozvgw.png', name: 'I\'ve Been Waiting', description: '\"What took you so long?\"', rarity: 'uncommon' }, + { link: 'https://imgur.com/DQDZFjA.png', name: 'Oops, You Blinked!', description: 'Come on Jeffrey, you have to keep your eye\'s open!', rarity: 'uncommon' }, + { link: 'https://imgur.com/15p7sDo.png', name: 'Basement Time?', description: '\"Human, we shall explore the basement.\"', rarity: 'uncommon' }, + { link: 'https://imgur.com/ASOaLa7.png', name: 'Scenic View', description: 'Jeffrey thought this would be the perfect place for a picture!', rarity: 'uncommon' }, + { link: 'https://imgur.com/kSo1xYB.png', name: 'On The Freezer', description: 'It turns out freezers can actually emit heat!', rarity: 'uncommon' }, + { link: 'https://imgur.com/vWkhS1P.png', name: 'Ceiling', description: 'This might be the highest he\'s ever been!', rarity: 'uncommon' }, + { link: 'https://imgur.com/FirGg1T.png', name: 'Recon Tower', description: 'He\'ll let us know if he see\'s anything. Probably...', rarity: 'uncommon' }, + { link: 'https://imgur.com/WVAcNOm.png', name: 'Linen Closet', description: 'My mom was not impressed.', rarity: 'uncommon' }, + { link: 'https://imgur.com/57yMtrl.png', name: 'Flower', description: 'Jeffrey\'s the REAL flower!', rarity: 'uncommon' }, + { link: 'https://imgur.com/j00IjET.png', name: 'Blurry Carpet', description: 'He\'ll sleep anywhere, and in any quality!', rarity: 'uncommon' }, + { link: 'https://imgur.com/DnWGWJS.png', name: 'Peak-At', description: 'Look at his cute little face under the monitor!', rarity: 'uncommon' }, + { link: 'https://imgur.com/OqOol5X.png', name: 'Trap', description: 'I wouldn\'t try to pet him right now...', rarity: 'uncommon' }, + { link: 'https://imgur.com/xryAFJi.png', name: 'Heater', description: 'CAT CRAVE HEAT', rarity: 'uncommon' }, + { link: 'https://imgur.com/WYGcMzt.png', name: 'Face-Cover', description: 'A make-shift light blocker!', rarity: 'uncommon' }, + { link: 'https://imgur.com/jhIC3Js.png', name: 'Arching', description: 'Jeffrey knows he\'s not suppose to be on the table, but he\'s playing it cool.', rarity: 'uncommon' }, + { link: 'https://imgur.com/Qh7TDY3.png', name: 'Pixel', description: 'Low quality, but still cute!', rarity: 'uncommon' }, + { link: 'https://imgur.com/voMHJ9n.png', name: 'Pillow', description: 'Kind of a weird spot, but ok Jeffrey.', rarity: 'uncommon' }, + { link: 'https://imgur.com/z81zoZ9.png', name: 'Alert', description: 'A very young and very alert Jeffrey.', rarity: 'uncommon' }, + { link: 'https://imgur.com/mQB4xHr.png', name: 'Cat House', description: 'His first cat house (and my phones lockscreen).', rarity: 'uncommon' }, + + { link: 'https://imgur.com/eNx2Ntg.png', name: 'Snickering', description: 'I know he\'s actually just sleeping, but I can pretend.', rarity: 'rare' }, + { link: 'https://imgur.com/moIgaas.png', name: 'Calm Collar', description: 'It didn\'t work very well, Jeffrey and Kitty still hated eachother.', rarity: 'rare' }, + { link: 'https://imgur.com/M2wJBt9.png', name: 'Birthday Party', description: 'He loves baloons!', rarity: 'rare' }, + { link: 'https://imgur.com/NaKE5Y2.png', name: 'Weird', description: 'What a strange little creature!', rarity: 'rare' }, + { link: 'https://imgur.com/KBwG10J.png', name: 'Pillowcase', description: 'That pillow fell on the floor overnight.', rarity: 'rare' }, + { link: 'https://imgur.com/D1cuR7k.png', name: 'BOX - Kitty vs', description: 'I wonder if they know...', rarity: 'rare' }, + { link: 'https://imgur.com/rt6lIPt.png', name: 'DOUBLE BED - Kitty vs', description: 'A bed on a bed? Jeffrey wants it!', rarity: 'rare' }, + { link: 'https://imgur.com/vvWZ384.png', name: 'MIRRIORED - Kitty vs', description: '\"She\'s right behind me, isn\'t she?\"', rarity: 'rare' }, + { link: 'https://imgur.com/UFNSYrP.png', name: 'TV - Kitty vs', description: 'They just can\'t agree on what to watch!', rarity: 'rare' }, + { link: 'https://imgur.com/CKfOGtA.png', name: 'STARE - Kitty vs', description: 'Kitty\'s staring daggers at our boy!', rarity: 'rare' }, + { link: 'https://imgur.com/dVGsAIo.png', name: 'WATER BOTTLE - Kitty vs', description: 'So, is the water bottle actually doing anything here?', rarity: 'rare' }, + { link: 'https://imgur.com/JujU88m.png', name: 'Box + Boxes', description: 'Jeffrey does love his boxes!', rarity: 'rare' }, + { link: 'https://imgur.com/KnTrE2e.png', name: 'TOWERS - Kitty vs', description: 'A very common scene, that usually led to the same outcome... destruction.', rarity: 'rare' }, + { link: 'https://imgur.com/yjh3yZg.png', name: 'Box Trap', description: 'He\'s ready for shipment!', rarity: 'rare' }, + { link: 'https://imgur.com/BbtiZwy.png', name: 'From Above', description: 'It\'s over Jeffrey, I have the high ground!', rarity: 'rare' }, + { link: 'https://imgur.com/LijfPss.png', name: 'Back Window', description: 'I feel like this shouldn\'t be named that...', rarity: 'rare' }, + { link: 'https://imgur.com/GnnuMI5.png', name: 'Aww', description: 'This picture speaks for itself!', rarity: 'rare' }, + { link: 'https://imgur.com/eGPRhm5.png', name: 'First Cat Bed', description: 'We didn\'t even tell him it was his yet!', rarity: 'rare' }, + { link: 'https://imgur.com/l8uOmAq.png', name: 'Father', description: 'Jeffrey\'s the new Dad!', rarity: 'rare' }, + + { link: 'https://imgur.com/miHJetv.png', name: 'Radiant', description: 'Our Lord and Savior Jeffrey', rarity: 'legendary' }, + { link: 'https://imgur.com/tbS4faL.png', name: 'Cannibal', description: 'OK FINE ILL FEED YOU NOW!', rarity: 'legendary' }, + { link: 'https://imgur.com/5QfM3zQ.png', name: 'Dead', description: 'RIP. Fly high king', rarity: 'legendary' }, + { link: 'https://imgur.com/xhWXOIA.png', name: 'Box', description: 'Wow, soooo original Jeffrey. Never seen a cat do THAT before.', rarity: 'legendary' }, + { link: 'https://imgur.com/okvejbp.png', name: 'Disturbed', description: 'I don\'t think he\'s a fan...', rarity: 'legendary' }, + { link: 'https://imgur.com/sdLYctZ.png', name: 'Cuddle', description: 'Wittle Cutie!', rarity: 'legendary' }, + { link: 'https://imgur.com/CYvTDSC.png', name: 'Not', description: 'Hey, that\'s not Jeffrey!', rarity: 'legendary' }, + { link: 'https://imgur.com/jmKtrlf.png', name: 'Peaceful', description: 'That\'s about as peaceful as its gonna get...', rarity: 'legendary' }, + { link: 'https://imgur.com/Y1i2gMP.png', name: 'Attack', description: 'That bite\'s lethal!', rarity: 'legendary' }, + { link: 'https://imgur.com/3aWCqqe.png', name: 'Caught', description: 'Hey, what\'s going on over here!', rarity: 'legendary' }, + { link: 'https://imgur.com/eTGaGLM.png', name: 'Lap-Cat', description: 'He\'s been known to peruse a lap or two.', rarity: 'legendary' }, + { link: 'https://imgur.com/7YdKWjS.png', name: 'GateKeeper', description: 'Come on Jeffrey, that\'s not very nice!', rarity: 'legendary' } +]; + diff --git a/src/index.ts b/src/index.ts index 9f56bbf..6d51c72 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,24 +3,14 @@ import { config } from 'dotenv'; import { Client, ClientOptions, - REST, - Routes, - GatewayIntentBits, - ChatInputCommandInteraction + GatewayIntentBits } from 'discord.js'; -import { Ping } from './commands/ping'; import { Mock } from './commands/mock'; -import { Cat } from './commands/cat'; -import { Balance } from './commands/balance'; -import { Poll } from './commands/poll'; -import { Roll } from './commands/roll'; -import { DB } from './JeffreyDB'; +import { DB } from './databse/JeffreyDB'; +import { checkAndRunCommand, initCmds } from './utilities/cmdUtils'; config(); -const cooldown = new Map>(); -const cooldownTime = 5000; - const token = process.env.BOT_TOKEN; const clientID = process.env.CLIENT_ID; const guildID = process.env.GUILD_ID; @@ -29,15 +19,12 @@ const intents = [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildMessages, - GatewayIntentBits.MessageContent, -] + GatewayIntentBits.MessageContent +]; -const options: ClientOptions = { - intents: intents, -}; +const options: ClientOptions = { intents: intents }; const client = new Client(options); - client.login(token); client.once('ready', () => { @@ -46,74 +33,19 @@ client.once('ready', () => { DB.sync(); }); -const rest = new REST({ version: '10' }).setToken(token!); - async function main() { try { console.log('Started refreshing application (/) commands.'); - - await rest.put( - Routes.applicationGuildCommands(clientID!, guildID!), - { - body: [ - Ping.info.toJSON(), - Mock.info.toJSON(), - Cat.info.toJSON(), - Poll.info.toJSON(), - Balance.info.toJSON(), - Roll.info.toJSON() - ] - } - ); - + await initCmds(token!, clientID!, guildID!); console.log('Successfully reloaded application (/) commands.'); } catch (error) { console.error(error); } -} -Poll.addChoiceOptions(); +}; client.on('interactionCreate', async (interaction) => { - if (!interaction.isCommand()) return; - - const { commandName } = interaction; - const userID = interaction.user.id; - - if (!cooldown.has(commandName)) { - cooldown.set(commandName, new Map()); - } - const cooldownMap = cooldown.get(commandName)!; - - if (cooldownMap.has(userID) && cooldownMap.get(userID)! > Date.now() && interaction.user.id !== '218823980524634112') { - const cooldownRemaining = (cooldownMap.get(userID)! - Date.now()) / 1000; - await interaction.reply(`Please wait ${cooldownRemaining.toFixed(1)} seconds.`); - return; - } - cooldownMap.set(userID, Date.now() + cooldownTime); - - console.log(`user ${interaction.user.username} (${userID}) ran the '${commandName}' command | Guild: ${interaction.guild} |` - + ` Channel: ${interaction.channel} | Timestamp: ${interaction.createdAt}`); - - if (commandName === 'ping') { - await Ping.run(interaction); - } - if (commandName === 'mock') { - console.log(`${interaction.user} is attempting to use the mock command.`); - await Mock.run(interaction); - console.log(`Current mock list: ${Array.from(mockTargets)}`); - } - if (commandName === 'cat') { - await Cat.run(interaction); - } - if (commandName === 'poll') { - Poll.run(interaction as ChatInputCommandInteraction); - } - if (commandName === 'balance') { - await Balance.run(interaction as ChatInputCommandInteraction); - } - if (commandName === 'roll') { - await Roll.run(interaction) - } + if (!interaction.isChatInputCommand()) return; + await checkAndRunCommand(interaction); }); client.on('messageCreate', async (message) => { diff --git a/src/utilities/cmdUtils.ts b/src/utilities/cmdUtils.ts new file mode 100644 index 0000000..49ef708 --- /dev/null +++ b/src/utilities/cmdUtils.ts @@ -0,0 +1,94 @@ +import { ChatInputCommandInteraction, REST, Routes } from "discord.js"; +import { Balance } from "../commands/balance"; +import { Cat } from "../commands/cat"; +import { DM } from "../commands/dm"; +import { Mock } from "../commands/mock"; +import { Ping } from "../commands/ping"; +import { Poll } from "../commands/poll"; +import { Roll } from "../commands/roll"; + +//individual command cooldown/rate limit +const cooldown = new Map>(); +const cooldownTime = 3000; + +//List of all commands +const CMD_LIST = [ + Ping, + Mock, + Cat, + Poll, + Balance, + Roll, + DM +]; +/** + * Function to initialize all of the bots commands (making them usable/visable to discord users) + * Also uses to add choice options for commands that need it. + * + * @param {string} token - bot token + * @param {string} clientID - bots discord account ID + * @param {string} guildID - discord server ID + */ +export async function initCmds(token: string, clientID: string, guildID: string): Promise { + const rest = new REST({ version: '10' }).setToken(token); + + Poll.addChoiceOptions(); + DM.addChoiceOptions(); + + const cmdInfo = CMD_LIST.map(cmd => cmd.info.toJSON()); + await rest.put( + Routes.applicationGuildCommands(clientID, guildID), + { + body: cmdInfo + } + ); +}; +/** + * Function to be called on 'interactionCreate' event. + * Checks which command was used, and runs said command. + * + * @param {ChatInputCommandInteraction} interaction - A slash command interaction + */ +export async function checkAndRunCommand(interaction: ChatInputCommandInteraction): Promise { + const { commandName } = interaction; + const userID = interaction.user.id; + + const cooldown = await checkCmdCD(commandName, userID); + if (!cooldown) { + console.log( + `User ${interaction.user.username} (${userID}) ran the '${commandName}' command | Guild: ${interaction.guild} |` + + ` Channel: ${interaction.channel} | Timestamp: ${interaction.createdAt.toUTCString()}` + ); + + for (let i = 0; i < CMD_LIST.length; i++) { + if (commandName === CMD_LIST[i].info.name) { + CMD_LIST[i].run(interaction); + } else { + continue; + } + } + } else { + await interaction.reply(`Please wait ${cooldown.toFixed(1)} seconds.`); + } +} +/** + * Function to check if the user is on cooldown for a specific command, when they try and use it. + * + * @param {string} commandName - Name of the command being used + * @param {string} userID - Discord ID of the command user + * @returns {Promise} - The time remaining on the users command cooldown. Returns 0 if it was not on cooldown. + */ +export async function checkCmdCD(commandName: string, userID: string): Promise { + if (!cooldown.has(commandName)) { + cooldown.set(commandName, new Map()); + } + const cooldownMap = cooldown.get(commandName)!; + + if (cooldownMap.has(userID) && cooldownMap.get(userID)! > Date.now()) { + const cooldownRemaining = (cooldownMap.get(userID)! - Date.now()) / 1000; + return cooldownRemaining; + } else { + cooldownMap.set(userID, Date.now() + cooldownTime); + return 0; + } +} \ No newline at end of file diff --git a/src/utilities/miscUtils.ts b/src/utilities/miscUtils.ts new file mode 100644 index 0000000..566cdfb --- /dev/null +++ b/src/utilities/miscUtils.ts @@ -0,0 +1,108 @@ +import { ChatInputCommandInteraction, EmbedBuilder, HexColorString, Message, User } from 'discord.js'; +import { BUTTONS } from '../constants/buttonConstants'; + +/** + * Chooses a random number between min and max + * @param {number} min - the lowest possible number + * @param {number} max - the highest possible number + * @returns {Promise} - returns the random number + */ +export async function rng(min: number, max: number): Promise { + const randomDecimal = Math.random(); + const range = max - min + 1; + const randomNumber = Math.floor(randomDecimal * range) + min; + return randomNumber; +}; + +/** + * Replies to the command user with an embedded message. + * Embed will be constructed in command files. + * @param {EmbedBuilder} embed - Embed with desired information/fields + * @param {ChatInputCommandInteraction} interaction - The command interaction to reply to. + */ +export async function replyWithEmbed(embed: EmbedBuilder, interaction: ChatInputCommandInteraction): Promise { + try { + await interaction.reply({ embeds: [embed] }); + console.log(`Replied with embed in ${interaction.channelId}`); + } catch { + console.log(`ERROR: could not send embed in ${interaction.channelId}`); + return; + } +}; + +/** + * Attempts to delete specified message. + * + * @param {Message} m - a discord message + * @returns {Promise} - Whether or not the message was successfully deleted + */ +export async function tryDelete(m: Message): Promise { + try { + m.delete(); + return true; + } catch (err) { + console.error('could not delete message', err); + return false; + } +}; + +/** + * Function that takes a previously created embed, and attempts to DM it to the specified member + * + * @param {EmbedBuilder} embed - The embed message to send. + * @param {User} member - Which member to dm + */ +export async function tryToDMEmbed(embed: EmbedBuilder, member: User): Promise { + try { + await member.send({ embeds: [embed] }); + console.log(`Sent message to ${member.id}`) + return true; + } catch (err) { + console.error(`Could not send message to ${member.id}`, err); + return false; + } +}; + +/** + * Function that takes a rarity and returns the color that the embed for that gacha rarity should be. + * + * @param {string} rarity - the rarity of the gacha to be displayed + * @returns {Promise} - the color(hexadecimal color string) associated with the given rarity + */ +export async function getEmbedColor(rarity: string): Promise { + let color: HexColorString; + if (rarity === 'common') { + color = `#808080`; //grey + } else if (rarity === 'uncommon') { + color = `#00A36C`; //green + } else if (rarity === 'rare') { + color = `#FF69B4`; //pink + } else if (rarity === 'legendary') { + color = `#D4A017`;// orange gold + } else { + color = `#000000` //black (shouldnt show up) + } + return color; +}; + +/** + * Function used specifically for situations where the user cycles through 'Next' and 'Previous' buttons. + * Checks if next/previous should be disabled or not. + * Previous - disabled if there is no place before it (position 0) + * Next - disabled if there is no place above it (current position === max/highest possible position) + * + * @param {number} currentPos - The current position in an array/list + * @param {number} max - The highest possible position/number + */ +export async function checkIfFirstOrLast(currentPos: number, max: number): Promise { + if (max - currentPos <= 1) { + BUTTONS.NEXT_BUTTON.setDisabled(true); + } else { + BUTTONS.NEXT_BUTTON.setDisabled(false); + } + if (currentPos < 1) { + BUTTONS.PREVIOUS_BUTTON.setDisabled(true); + } else { + BUTTONS.PREVIOUS_BUTTON.setDisabled(false); + } +}; \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index 388be66..0000000 --- a/src/utils.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { CommandInteraction, EmbedBuilder } from 'discord.js'; - -export const NUMBER_EMOJIS: string[] = [ - "1⃣", - "2⃣", - "3⃣", - "4⃣", - "5⃣", - "6⃣", - "7⃣", - "8⃣", - "9⃣", - "🔟" -]; -/** - * Chooses a random number between min and max - * @param min - the lowest possible number - * @param max - the highest possible number - * @returns - returns the random number - */ -export async function rng(min: number, max: number): Promise { - const randomDecimal = Math.random(); - - const range = max - min + 1; - const randomNumber = Math.floor(randomDecimal * range) + min; - - return randomNumber -} -/** - * Replies to the command user with an embedded message. - * Embed will be constructed in command files. - * @param embed - Embed with desired information/fields - * @param interaction - The command interaction to reply to. - */ -export async function replyWithEmbed(embed: EmbedBuilder, interaction: CommandInteraction): Promise { - try { - await interaction.reply({ embeds: [embed] }); - console.log(`replied with embed in ${interaction.channelId}`); - } catch { - console.log(`ERROR: could not send embed in ${interaction.channelId}`); - return; - } -} \ No newline at end of file