diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..6552a36 --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,22 @@ +name: Node.js CI/CD + +on: [push] # tells github to run this on any push to the repository + +jobs: + deploy: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' # we tell Github to only execute this step if we're on our master branch (so we don't put unfinished branches in production) + steps: + - name: Deploying to Ayrenn + uses: appleboy/ssh-action@master # An action made to control Linux servers + with: # We set all our secrets here for the action, these won't be shown in the action logs + host: ${{ secrets.HOST }} + username: ${{ secrets.USERNAME }} + key: ${{ secrets.KEY }} + port: ${{ secrets.PORT }} + script: | + cd nodbot-v3 # we move into our app's folder + git pull # we pull any changes from git + npm prune # we remove any unused dependencies + npm install # we install any missing dependencies + pm2 reload all # we reload the app via PM2 diff --git a/README.md b/README.md deleted file mode 100644 index a1291cf..0000000 --- a/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# NodBot -A simple Discord bot created by @voidf1sh#0420 for retreiving gifs, saving copypastas, and more coming soon. - -## Dependencies -NodBot depends on `fs`, `discord.js`, `dotenv`, `tenorjs`, `pg`, and `axios`. - -## Features -Dynamic Help Message -Ability to save favorite gifs and copypastas - -## Usage -All commands are provided as "file extensions" instead of prefixes to the message. - -``` -foo.gif -- Will return the first GIF for 'foo' -foo.savegif -- Will send the first GIF result for 'foo', with Reactions to browse the results and save the GIF -foo.savepasta -- Prompts the user for the copypasta text to save as 'foo.pasta' -foo.pasta -- If a copypasta by the name of 'foo' is saved, the bot will send it -foo.weather -- Returns the current weather in 'foo', where 'foo' is a city or ZIP code -foobar.spongebob - Returns 'FoObAr' aka SpongeBob text -.joint -- Puff, Puff, Pass. -``` - -## To Do -v3 TODO: -Create database for storage of gifs, pastas, joint phrases, etc. -Migrate to Discord.js v13 Beta -Implement Replies to messages -Implement buttons in lieu of Reacts - -DONE: Clean up text input for copypastas, line breaks and apostrophes break the bot. -Add ability to reload commands, gifs, pastas, etc without rebooting the bot manually. -DONE: Change `savepasta` to use a collector and ask for the name or the pasta. -Add Stock quotes from Yahoo Finance API -Add self-delete if wrongbad'd -Move most string literals to config.json or strings.json for ease of editing. -Make construction of the `data` element easier for the createEmbeds functions. -Find a Cannabis API for strain information lookup. diff --git a/ReleaseNotes.md b/ReleaseNotes.md deleted file mode 100644 index acffeaf..0000000 --- a/ReleaseNotes.md +++ /dev/null @@ -1,20 +0,0 @@ -# Release Notes - -## v2.2.1 Hotfix -Fix bug where saved content isn't saved as lowercase, making in unaccessible. - -## v2.2.0 -NodBot no longer stores saved GIFs, Copypastas, and other custom content locally. This means no more discrepancies between versions of the bot! - -## v2.1.0 -Want to add a phrase to the `.joint` rotation? Try `.roll`. - -Wondering what GIFs and Copypastas have been saved? Try `.gifs` and `.pastas`, also check out the new help message with `.help`! - -NodBot now uses Tenor instead of Giphy for GIF searches! - -Changing the method to search for and save GIFs for later reuse. Previously the bot simply sent a message containing the link to a GIF which Discord would display in the chat. However the new code uses Embeds to make the messages look prettier. These Embeds require a *direct* link to the GIF, which isn't very user friendly. Now you can search for a GIF and NodBot will DM you with results for you to browse before choosing the GIF you'd like to save, then name it. - -Generic bug squashing, sanitizing of inputs, setting up some configs for deployment on Heroku. - -Updating Copypasta save method to be interactive and give a less complicated command syntax. \ No newline at end of file diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..c94e74d --- /dev/null +++ b/TODO.md @@ -0,0 +1,29 @@ +[ v3.0.0 ] +*= Finish MySQL Migration +* = Import strain names to a collection +* = Implement fuzzy-search for strain lookup +* = Then pass confirmed-good strainName to fn.download.strain() +*= Test all functions +* = Write out the process used to test functionality to standardize it. += Sanitize inputs + = Don't forget to test apostrophes and newlines. + = Emoji break strain lookup + = Find a way to filter out URLs that may have .extensions at the end + = Mentions in .strain crash += Fix newline escaping in savepasta += Check what inputs need to be sanitized +! Name checking for saving content ! + +[ v3.1.0 ] += .jpg, .wav += Audio/Video attachments for saved content. += Pass The Joint += Voting system for Super Adventure Club + + +[ v4.0.0 ] += Scalability: modify the code to allow the bot to be used in multiple servers + = including saved content, saved commands, preferences, etc. + +[ v?.?.? ] += Joke generator for Hallihan \ No newline at end of file diff --git a/_deploy-commands.js b/_deploy-commands.js new file mode 100644 index 0000000..b78e955 --- /dev/null +++ b/_deploy-commands.js @@ -0,0 +1,39 @@ +// dotenv for handling environment variables +const dotenv = require('dotenv'); +dotenv.config(); + +const { REST } = require('@discordjs/rest'); +const { Routes } = require('discord-api-types/v9'); +const { guildId, clientId } = require('./config.json'); +const token = process.env.TOKEN; +const fs = require('fs'); + +const commands = []; +const commandFiles = fs.readdirSync('./slash-commands').filter(file => file.endsWith('.js')); + +for (const file of commandFiles) { + const command = require(`./slash-commands/${file}`); + if (command.data != undefined) { + commands.push(command.data.toJSON()); + } +} + +console.log(commands); + +const rest = new REST({ version: '9' }).setToken(token); + +(async () => { + try { + console.log('Started refreshing application (/) commands.'); + + await rest.put( + Routes.applicationGuildCommands(clientId, guildId), + { body: commands }, + ); + + console.log('Successfully reloaded application (/) commands.'); + process.exit(); + } catch (error) { + console.error(error); + } +})(); \ No newline at end of file diff --git a/commands/airport.js b/commands/airport.js deleted file mode 100644 index cb89fd9..0000000 --- a/commands/airport.js +++ /dev/null @@ -1,27 +0,0 @@ -const axios = require("axios").default; -const functions = require('../functions.js'); - -let options = { - method: 'GET', - url: 'https://forteweb-airportguide-airport-basic-info-v1.p.rapidapi.com/get_airport_by_iata', - params: {auth: 'authairport567', airport_id: 'LAX'}, - headers: { - 'x-rapidapi-key': '0b3f85bcb7msh1e6e80e963c9914p1d1934jsnc3542fc83520', - 'x-rapidapi-host': 'forteweb-airportguide-airport-basic-info-v1.p.rapidapi.com' - } -}; - -module.exports = { - name: 'airport', - description: 'Get airport information by IATA code.', - usage: '', - execute(message, file) { - options.params.airport_id = file.name; - axios.request(options).then(function (response) { - const embed = functions.createAirportEmbed(response.data, message.author, `${file.name}.${file.extension}`); - message.channel.send(embed).then().catch(err => console.error(err)); - }).catch(function (error) { - console.error(error); - }); - } -} \ No newline at end of file diff --git a/commands/closereq.js b/commands/closereq.js deleted file mode 100644 index 096aeae..0000000 --- a/commands/closereq.js +++ /dev/null @@ -1,11 +0,0 @@ -const fn = require('../functions.js'); - -module.exports = { - name: 'closereq', - description: 'Close a given request by ID', - usage: '', - execute(message, file) { - fn.closeRequest(file.name); - message.channel.send(fn.textEmbed('Request closed.', message.author, file.extension)); - } -} \ No newline at end of file diff --git a/commands/gif.js b/commands/gif.js deleted file mode 100644 index 289c0bb..0000000 --- a/commands/gif.js +++ /dev/null @@ -1,38 +0,0 @@ -const functions = require('../functions'); -const tenor = require('tenorjs').client({ - "Key": process.env.tenorAPIKey, // https://tenor.com/developer/keyregistration - "Filter": "off", // "off", "low", "medium", "high", not case sensitive - "Locale": "en_US", // Your locale here, case-sensitivity depends on input - "MediaFilter": "minimal", // either minimal or basic, not case sensitive - "DateFormat": "D/MM/YYYY - H:mm:ss A" // Change this accordingly -}); - -module.exports = { - name: 'gif', - description: 'Send a GIF', - usage: '', - execute(message, file) { - const client = message.client; - if (!client.gifs.has(file.name)) { - tenor.Search.Query(file.name, 1).then(res => { - if (res[0] == undefined) { - message.reply('Sorry I was unable to find a GIF of ' + file.name); - return; - }; - const gifInfo = { - 'name': file.name, - 'embed_url': res[0].media[0].gif.url - }; - message.channel.send(functions.createGifEmbed(gifInfo, message.author, `${file.name}.${file.extension} - Tenor`)); - }) - .catch(err => console.error(err)); - } else { - // message.channel.send(file.name + ' requested by ' + message.author.username + '\n' + client.gifs.get(file.name).embed_url); - const gifInfo = { - 'name': file.name, - 'embed_url': client.gifs.get(file.name).embed_url - }; - message.channel.send(functions.createGifEmbed(gifInfo, message.author, `${file.name}.${file.extension} - Saved`)); - } - } -} \ No newline at end of file diff --git a/commands/gifs.js b/commands/gifs.js deleted file mode 100644 index 3bef0ac..0000000 --- a/commands/gifs.js +++ /dev/null @@ -1,12 +0,0 @@ -const functions = require('../functions.js'); - -module.exports = { - name: 'gifs', - description: 'Get a list of saved GIFs', - execute(message, file) { - message.author.createDM().then(channel => { - channel.send(functions.createGIFList(message)); - message.reply('I\'ve sent you a DM with a list of saved GIFs.') - }).catch(err => message.channel.send('Sorry I was unable to send you a DM.')); - } -} \ No newline at end of file diff --git a/commands/help.js b/commands/help.js deleted file mode 100644 index 19e24cb..0000000 --- a/commands/help.js +++ /dev/null @@ -1,13 +0,0 @@ -const functions = require('../functions.js'); - -module.exports = { - name: 'help', - description: 'Shows the help page.', - execute(message, file) { - message.author.createDM() - .then(dmChannel => { - dmChannel.send(functions.createHelpEmbed(message)).then().catch(err => console.error(err)); - message.reply('I\'ve DM\'d you a copy of my help message!'); - }).catch(err => console.error(err)); - }, -}; \ No newline at end of file diff --git a/commands/joint.js b/commands/joint.js deleted file mode 100644 index 55c0940..0000000 --- a/commands/joint.js +++ /dev/null @@ -1,14 +0,0 @@ -const { emoji } = require('../src/strings.json'); - -module.exports = { - name: 'joint', - description: 'Pass the joint!', - execute(message, args) { - let phrases = []; - for (const entry of message.client.potphrases.map(potphrase => potphrase.content)) { - phrases.push(entry); - } - const randIndex = Math.floor(Math.random() * phrases.length); - message.channel.send(`${phrases[randIndex]} ${emoji.joint}`); - } -} \ No newline at end of file diff --git a/commands/joints.js b/commands/joints.js deleted file mode 100644 index c6fb555..0000000 --- a/commands/joints.js +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = { - name: 'joints', - description: 'Get a list of the phrases saved for .joint', - execute(message, file) { - let phrases = []; - - for (const phrase of message.client.potphrases.map(potphrase => potphrase.content)) { - phrases.push(phrase); - } - message.channel.send('Here are all the `.joint` phrases I have saved:\n\n' + phrases.join('\n')); - } -} \ No newline at end of file diff --git a/commands/kill.js b/commands/kill.js deleted file mode 100644 index c124080..0000000 --- a/commands/kill.js +++ /dev/null @@ -1,24 +0,0 @@ -const ownerID = process.env.ownerID; - -module.exports = { - name: 'kill', - description: 'Kills the bot OWNER ONLY', - execute(message, args) { - if (message.author.id == ownerID) { - message.channel.send('Shutting down the bot...') - .then(() => { - message.client.destroy(); - process.exit(); - }); - } else { - message.reply('Sorry, only the owner can do that.'); - message.client.users.fetch(ownerID) - .then(user => { - user.send(message.author.username + ' attempted to shutdown the bot.') - .then() - .catch(err => console.error(err)); - }) - .catch(err => console.error(err)); - } - } -} \ No newline at end of file diff --git a/commands/pasta.js b/commands/pasta.js deleted file mode 100644 index 9276108..0000000 --- a/commands/pasta.js +++ /dev/null @@ -1,20 +0,0 @@ -const functions = require('../functions.js'); - -module.exports = { - name: 'pasta', - description: 'Send a copypasta.', - usage: '', - execute(message, file) { - const client = message.client; - const replyHeader = `\'${file.name}\' requested by: ${message.author.username}\n`; - let replyBody = ''; - let iconUrl; - if (!client.pastas.has(file.name)) { - replyBody = 'Sorry I couldn\'t find that pasta.'; - } else { - replyBody = client.pastas.get(file.name).content; - iconUrl = client.pastas.get(file.name).iconUrl; - } - message.channel.send(functions.pastaEmbed(replyBody, iconUrl, message.author)); - } -} \ No newline at end of file diff --git a/commands/pastas.js b/commands/pastas.js deleted file mode 100644 index c7fbe05..0000000 --- a/commands/pastas.js +++ /dev/null @@ -1,12 +0,0 @@ -const functions = require('../functions.js'); - -module.exports = { - name: 'pastas', - description: 'Get a list of saved copypastas', - execute(message, file) { - message.author.createDM().then(channel => { - channel.send(functions.createPastaList(message)); - message.channel.send('I\'ve sent you a DM with a list of saved copypastas.') - }).catch(err => message.channel.send('Sorry I was unable to send you a DM.')); - } -} \ No newline at end of file diff --git a/commands/ping.js b/commands/ping.js deleted file mode 100644 index d9035ae..0000000 --- a/commands/ping.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - name: 'ping', - description: 'Pong!', - execute(message, args) { - message.channel.send('Pong!'); - } -} \ No newline at end of file diff --git a/commands/reload.js b/commands/reload.js deleted file mode 100644 index 97f4320..0000000 --- a/commands/reload.js +++ /dev/null @@ -1,10 +0,0 @@ -const fn = require('../functions'); - -module.exports = { - name: 'reload', - description: 'Reload saved GIFs, Pastas, Joint Phrases, etc', - execute(message, file) { - fn.reload(message.client); - message.reply('Reload Successful'); - } -} \ No newline at end of file diff --git a/commands/request.js b/commands/request.js deleted file mode 100644 index 93d401c..0000000 --- a/commands/request.js +++ /dev/null @@ -1,13 +0,0 @@ -const fn = require('../functions.js'); - -module.exports = { - name: 'request', - description: 'Submit a request to the bot developer.', - usage: '', - execute(message, file) { - const request = file.name; - message.channel.send(fn.textEmbed('Your request has been submitted!\nRequest: ' + request, message.author, file.extension)); - message.client.users.fetch(process.env.ownerID).then(user => {user.send(fn.textEmbed(request, message.author, file.extension));}).catch(error => { console.error(error);} ); - fn.uploadRequest(message.author, file.name); - } -} \ No newline at end of file diff --git a/commands/requests.js b/commands/requests.js deleted file mode 100644 index 0a00aac..0000000 --- a/commands/requests.js +++ /dev/null @@ -1,9 +0,0 @@ -const fn = require('../functions.js'); - -module.exports = { - name: 'requests', - description: 'Get a list of the currently active requests.', - execute(message, file) { - fn.getActiveRequests(message); - } -} \ No newline at end of file diff --git a/commands/roll.js b/commands/roll.js deleted file mode 100644 index cd6a333..0000000 --- a/commands/roll.js +++ /dev/null @@ -1,11 +0,0 @@ -const functions = require('../functions.js'); - -module.exports = { - name: 'roll', - description: 'Add a phrase to the .joint command', - usage: '', - execute(message, file) { - functions.uploadPotPhrase(file.name); - message.channel.send('"' + file.name + '" has been added to the list'); - } -} \ No newline at end of file diff --git a/commands/savegif.js b/commands/savegif.js deleted file mode 100644 index 8034010..0000000 --- a/commands/savegif.js +++ /dev/null @@ -1,136 +0,0 @@ -const tenor = require('tenorjs').client({ - "Key": process.env.tenorAPIKey, // https://tenor.com/developer/keyregistration - "Filter": "off", // "off", "low", "medium", "high", not case sensitive - "Locale": "en_US", // Your locale here, case-sensitivity depends on input - "MediaFilter": "minimal", // either minimal or basic, not case sensitive - "DateFormat": "D/MM/YYYY - H:mm:ss A" // Change this accordingly -}); -const functions = require('../functions'); -const { emoji } = require('../src/strings.json'); - -module.exports = { - name: 'savegif', - description: 'Saves a gif selected from a search to a given filename.', - usage: '', - execute(message, file) { - const query = file.name; - message.author.createDM().then(channel => { - tenor.Search.Query(query, 20) - .then(res => { - if (res[0] == undefined) { - channel.send('Sorry, I wasn\'t able to find a GIF of ' + file.name); - return; - } - let i = 0; - const data = { - "name": file.name, - "embed_url": res[0].media[0].gif.url, - "author": message.author - }; - let embed = functions.createGifEmbed(data, message.author, `${Object.values(file).join('.')}`); - - // Send the first GIF result as an Embed - channel.send(embed) - .then(selfMessage => { - // Add reactions to go back, forward, cancel and confirm GIF choice. - // React order is important so these are done in a chain - selfMessage.react(emoji.previous).then(() => { - selfMessage.react(emoji.confirm).then(() => { - selfMessage.react(emoji.next).then(() => { - selfMessage.react(emoji.cancel); - }); - }); - }); - const filter = (reaction, user) => { - return ((reaction.emoji.name == emoji.next) || (reaction.emoji.name == emoji.confirm) || (reaction.emoji.name == emoji.previous) || (reaction.emoji.name == emoji.cancel)) && user.id == message.author.id; - } - const collector = selfMessage.createReactionCollector(filter, { time: 120000 }); - - collector.on('collect', (reaction, user) => { - switch (reaction.emoji.name) { - case emoji.next: - if (i < res.length) { - i++; - } else { - selfMessage.channel.send('That\'s the last GIF, sorry!'); - break; - } - data.embed_url = res[i].media[0].gif.url; - embed = functions.createGifEmbed(data, message.author, `${file.name}.${file.extension}`); - if (selfMessage.editable) { - selfMessage.edit(embed); - } - break; - case emoji.confirm: - channel.send('GIF Selected. What should I save the GIF as? (don\'t include the `.gif`)\nReact with ' + emoji.cancel + ' to cancel.') - .then(nameQueryMessage => { - nameQueryMessage.react(emoji.cancel); - const cancelReactFilter = (reaction, user) => { - return (reaction.emoji.name == emoji.cancel) && (user.id == message.author.id); - } - const cancelReactCollector = nameQueryMessage.createReactionCollector(cancelReactFilter, { time: 20000, max: 1 }); - - cancelReactCollector.on('collect', (reaction, user) => { - nameCollector.stop('cancel'); - if (selfMessage.deletable) selfMessage.delete(); - if (nameQueryMessage.deletable) nameQueryMessage.delete(); - }) - const nameCollectorFilter = nameMessage => nameMessage.author == message.author; - const nameCollector = nameQueryMessage.channel.createMessageCollector(nameCollectorFilter, { time: 30000, max: 1 }); - - nameCollector.on('collect', nameMessage => { - channel.send('The GIF has been saved as: ' + nameMessage.content + '.gif'); - functions.saveGif(message, nameMessage.content.toLowerCase(), data.embed_url); - }); - nameCollector.on('end', (collected, reason) => { - switch (reason) { - case 'cancel': - channel.send('The action has been canceled.'); - break; - default: - break; - } - }); - }); - collector.stop("confirm"); - break; - case emoji.previous: - if (i > 0) { - i--; - } else { - selfMessage.channel.send('That\'s the first GIF, can\'t go back any further!'); - break; - } - data.embed_url = res[i].media[0].gif.url; - embed = functions.createGifEmbed(data, message.author, `${file.name}.${file.extension}`); - if (selfMessage.editable) { - selfMessage.edit(embed); - } - break; - case emoji.cancel: - collector.stop('cancel'); - break; - default: - channel.send('There was an error, sorry.'); - break; - } - }); - - collector.on('end', (collected, reason) => { - switch (reason) { - case 'cancel': - selfMessage.delete(); - channel.send('The action has been canceled.'); - break; - case 'messageDelete': - break; - default: - break; - } - }) - }).catch(err => console.error(err)); - }) - .catch(err => console.error(err)); - }) - } -} \ No newline at end of file diff --git a/commands/savepasta.js b/commands/savepasta.js deleted file mode 100644 index a675084..0000000 --- a/commands/savepasta.js +++ /dev/null @@ -1,19 +0,0 @@ -const functions = require('../functions.js'); - -module.exports = { - name: 'savepasta', - description: 'Saves a copypasta as pasta_name.pasta, just send the pasta name on the first message, and the bot will ask for the actual pasta afterwards.', - usage: '', - execute(message, file) { - message.channel.send(`I'll be saving the next message you send as ${file.name}.pasta\nWhat is the content of the copypasta?`) - .then(promptMessage => { - const pastaFilter = pastaMessage => pastaMessage.author == message.author; - const pastaCollector = promptMessage.channel.createMessageCollector(pastaFilter, { time: 30000, max: 1 }); - - pastaCollector.on('collect', pastaMessage => { - message.channel.send(functions.savePasta(message, file.name.toLowerCase(), functions.cleanInput(pastaMessage.content))); - }) - }) - .catch(err => console.error(err)); - } -} \ No newline at end of file diff --git a/commands/sendhelp.js b/commands/sendhelp.js deleted file mode 100644 index 991312e..0000000 --- a/commands/sendhelp.js +++ /dev/null @@ -1,10 +0,0 @@ -const fn = require('../functions.js'); - -module.exports = { - name: 'sendhelp', - description: 'Send the help message to the current channel', - permissions: 'BOT_MOD', // To be implemented later - execute(message, file) { - message.channel.send(fn.createHelpEmbed(message)); - } -} \ No newline at end of file diff --git a/commands/spongebob.js b/commands/spongebob.js deleted file mode 100644 index 727558a..0000000 --- a/commands/spongebob.js +++ /dev/null @@ -1,21 +0,0 @@ -const functions = require('../functions.js'); - -module.exports = { - name: 'spongebob', - description: 'SpOnGeBoB-iFy AnYtHiNg AuToMaTiCaLly', - usage: '', - execute(message, file) { - let flipper = 0; - let newText = ''; - for (const letter of file.name) { - if (flipper == 0) { - newText = newText + letter.toUpperCase(); - flipper = 1; - } else { - newText = newText + letter; - flipper = 0; - } - } - message.channel.send(`@${message.author.username}: ${newText}`); - } -} \ No newline at end of file diff --git a/commands/stonk.js b/commands/stonk.js deleted file mode 100644 index d388482..0000000 --- a/commands/stonk.js +++ /dev/null @@ -1,24 +0,0 @@ -var axios = require("axios").default; - -var options = { - method: 'GET', - url: 'https://yahoo-finance-low-latency.p.rapidapi.com/v6/finance/quote', - params: {symbols: ''}, - headers: { - 'x-rapidapi-key': '0b3f85bcb7msh1e6e80e963c9914p1d1934jsnc3542fc83520', - 'x-rapidapi-host': 'yahoo-finance-low-latency.p.rapidapi.com' - } -}; - -module.exports = { - name: 'stonk', - description: 'Get stonk details from Yahoo Finance', - execute(message, file) { - options.params.symbols = file.name; - axios.request(options).then(function (response) { - - }).catch(function (error) { - console.error(error); - }); - } -} \ No newline at end of file diff --git a/commands/strain.js b/commands/strain.js deleted file mode 100644 index db5bf51..0000000 --- a/commands/strain.js +++ /dev/null @@ -1,22 +0,0 @@ -const axios = require('axios').default; -const options = { - method: 'GET', - url: 'https://brianiswu-otreeba-open-cannabis-v1.p.rapidapi.com/strains', - headers: { - 'x-rapidapi-key': '0b3f85bcb7msh1e6e80e963c9914p1d1934jsnc3542fc83520', - 'x-rapidapi-host': 'brianiswu-otreeba-open-cannabis-v1.p.rapidapi.com' - } -}; - -module.exports = { - name: 'strain', - description: 'Search for information about a cannabis strain. Powered by Otreeba', - execute(message, file) { - options.url = options.url + '?name=' + file.name; - axios.request(options).then(function (response) { - console.log(response.data); - }).catch(function (error) { - console.error(error); - }); - } -} \ No newline at end of file diff --git a/commands/template b/commands/template deleted file mode 100644 index 5896e8e..0000000 --- a/commands/template +++ /dev/null @@ -1,9 +0,0 @@ -const fn = require('../functions.js'); - -module.exports = { - name: '', - description: '', - execute(message, file) { - - } -} \ No newline at end of file diff --git a/commands/truth.js b/commands/truth.js deleted file mode 100644 index fab5c0c..0000000 --- a/commands/truth.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - name: "truth", - description: "The truth about MHFS", - execute(message, args) { - message.channel.send("https://www.twitch.tv/hochmania/clip/EsteemedSlickDootStinkyCheese-hncmP8aIP8_WQb_a?filter=clips&range=all&sort=time"); - } -} \ No newline at end of file diff --git a/commands/upload.js b/commands/upload.js deleted file mode 100644 index 8e1bb87..0000000 --- a/commands/upload.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - name: 'upload', - description: '', - execute(message, file) { - const fn = require('../functions'); - - fn.uploadGIFs(message); - - message.reply('Uploaded, I hope.'); - } -} \ No newline at end of file diff --git a/commands/weather.js b/commands/weather.js deleted file mode 100644 index 1000c93..0000000 --- a/commands/weather.js +++ /dev/null @@ -1,27 +0,0 @@ -const functions = require('../functions.js'); -const axios = require("axios").default; - -var options = { - method: 'GET', - url: 'https://weatherapi-com.p.rapidapi.com/current.json', - params: {q: ''}, - headers: { - 'x-rapidapi-key': '0b3f85bcb7msh1e6e80e963c9914p1d1934jsnc3542fc83520', - 'x-rapidapi-host': 'weatherapi-com.p.rapidapi.com' - } -}; - -module.exports = { - name: 'weather', - description: 'Get the current weather by ZIP code or city name.', - usage: '', - execute(message, file) { - options.params.q = file.name; - axios.request(options).then(function (response) { - const embed = functions.createWeatherEmbed(response.data, message.author, `${file.name}.${file.extension}`); - message.channel.send(embed).then().catch(err => console.error(err)); - }).catch(function (error) { - console.error(error); - }); - } -} \ No newline at end of file diff --git a/commands/wrongbad.js b/commands/wrongbad.js deleted file mode 100644 index df735d6..0000000 --- a/commands/wrongbad.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - name: "wrongbad", - description: "", - execute(message, args) { - - const wrongbad = "<:wrongbad:853304921969393684>"; - message.channel.send(""); - } -} \ No newline at end of file diff --git a/config.json b/config.json index 4e7d04c..e917303 100644 --- a/config.json +++ b/config.json @@ -1,3 +1,8 @@ { - "validExtensions": [] + "isDev": true, + "clientId": "513184762073055252", + "guildId": "760701839427108874", + "devChannelId": "868545469381480498", + "guildName": "CumbHub", + "validCommands": [] } \ No newline at end of file diff --git a/dot-commands/gif.js b/dot-commands/gif.js new file mode 100644 index 0000000..0568a6a --- /dev/null +++ b/dot-commands/gif.js @@ -0,0 +1,36 @@ +const fn = require('../functions'); +const tenor = require('tenorjs').client({ + "Key": process.env.tenorAPIKey, // https://tenor.com/developer/keyregistration + "Filter": "off", // "off", "low", "medium", "high", not case sensitive + "Locale": "en_US", // Your locale here, case-sensitivity depends on input + "MediaFilter": "minimal", // either minimal or basic, not case sensitive + "DateFormat": "D/MM/YYYY - H:mm:ss A" // Change this accordingly +}); + +module.exports = { + name: 'gif', + description: 'Send a GIF', + usage: '.gif', + execute(message, commandData) { + if (message.deletable) message.delete(); + const client = message.client; + if (!client.gifs.has(commandData.args)) { + tenor.Search.Query(commandData.args, 1).then(res => { + if (res[0] == undefined) { + message.reply('Sorry I was unable to find a GIF of ' + commandData.args); + return; + }; + commandData.embed_url = res[0].media[0].gif.url; + // message.reply(fn.embeds.gif(commandData)); + message.channel.send(`> ${commandData.author} - ${commandData.args}.gif`); + message.channel.send(commandData.embed_url); + }).catch(err => console.error(err)); + } else { + // message.reply(commandData.args + ' requested by ' + message.author.username + '\n' + client.gifs.get(commandData.args).embed_url); + commandData.embed_url = client.gifs.get(commandData.args).embed_url; + // message.reply(fn.embeds.gif(commandData)); + message.channel.send(`> ${commandData.author} - ${commandData.args}.gif`); + message.channel.send(commandData.embed_url); + } + } +} \ No newline at end of file diff --git a/dot-commands/pasta.js b/dot-commands/pasta.js new file mode 100644 index 0000000..247cf96 --- /dev/null +++ b/dot-commands/pasta.js @@ -0,0 +1,19 @@ +const fn = require('../functions.js'); + +module.exports = { + name: 'pasta', + description: 'Send a copypasta.', + usage: '.pasta', + execute(message, commandData) { + const client = message.client; + let replyBody = ''; + let iconUrl; + if (!client.pastas.has(commandData.args)) { + commandData.content = 'Sorry I couldn\'t find that pasta.'; + } else { + commandData.content = client.pastas.get(commandData.args).content; + commandData.iconUrl = client.pastas.get(commandData.args).iconUrl; + } + message.reply(fn.embeds.pasta(commandData)); + } +} \ No newline at end of file diff --git a/dot-commands/request.js b/dot-commands/request.js new file mode 100644 index 0000000..8f9c2ca --- /dev/null +++ b/dot-commands/request.js @@ -0,0 +1,17 @@ +const fn = require('../functions.js'); + +module.exports = { + name: 'request', + description: 'Submit a request to the bot developer.', + usage: '.request', + execute(message, commandData) { + const request = commandData.args; + commandData.content = `Your request has been submitted!\nRequest: ${request}`; + message.reply(fn.embeds.text(commandData)); + commandData.content = `A new request has been submitted by ${message.author.tag}:\n${commandData.args}`; + message.client.users.fetch(process.env.ownerID).then(user => { + user.send(fn.embeds.text(commandData)); + }).catch(error => { console.error(error); }); + fn.upload.request(commandData, message.client); + }, +}; \ No newline at end of file diff --git a/dot-commands/spongebob.js b/dot-commands/spongebob.js new file mode 100644 index 0000000..d4bdaa9 --- /dev/null +++ b/dot-commands/spongebob.js @@ -0,0 +1,34 @@ +const functions = require('../functions.js'); +const config = require('../config.json'); + +module.exports = { + name: 'spongebob', + description: 'SpOnGeBoB-iFy AnYtHiNg AuToMaTiCaLly', + usage: '.spongebob', + execute(message, commandData) { + let flipper = 0; + let newText = ''; + for (const letter of commandData.args) { + if (letter == ' ') { + newText = newText + letter; + continue; + } + if (letter == 'i' || letter == 'I') { + newText = newText + 'i'; + continue; + } + if (letter == 'l' || letter == 'L') { + newText = newText + 'L'; + continue; + } + if (flipper == 0) { + newText = newText + letter.toUpperCase(); + flipper = 1; + } else { + newText = newText + letter; + flipper = 0; + } + } + message.reply(`@${message.author.username}: ${newText}`); + } +} \ No newline at end of file diff --git a/dot-commands/strain.js b/dot-commands/strain.js new file mode 100644 index 0000000..630a4b4 --- /dev/null +++ b/dot-commands/strain.js @@ -0,0 +1,17 @@ +const fn = require('../functions'); + +module.exports = { + name: 'strain', + description: 'Search for information about a cannabis strain.', + usage: '.strain', + execute(message, commandData) { + commandData.strainName = fn.weed.strain.lookup(commandData.args, message.client); + if (commandData.strainName) { + fn.download.strain(commandData, message); + } + else { + commandData.content = 'Sorry, I couldn\'t find a strain with that name: ' + commandData.args; + message.reply(fn.embeds.text(commandData)); + } + } +} \ No newline at end of file diff --git a/functions.js b/functions.js index 6d43a74..2214173 100644 --- a/functions.js +++ b/functions.js @@ -1,304 +1,439 @@ -const Discord = require('discord.js'); -const fs = require('fs'); -const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js')); -const config = require('./config.json'); -const pg = require('pg'); -let dbConnected = false; -const db = new pg.Client({ - connectionString: process.env.DATABASE_URL, - ssl: { - rejectUnauthorized: false - } -}); +/* eslint-disable comma-dangle */ +// dotenv for handling environment variables +const dotenv = require('dotenv'); +dotenv.config(); +// Assignment of environment variables +const dbHost = process.env.dbHost; +const dbUser = process.env.dbUser; +const dbName = process.env.dbName; +const dbPass = process.env.dbPass; +const dbPort = process.env.dbPort; +// filesystem +const fs = require('fs'); + +// D.js +const Discord = require('discord.js'); + +// Fuzzy text matching for db lookups +const FuzzySearch = require('fuzzy-search'); + +// Various imports +const config = require('./config.json'); +const strings = require('./strings.json'); +const slashCommandFiles = fs.readdirSync('./slash-commands/').filter(file => file.endsWith('.js')); +const dotCommandFiles = fs.readdirSync('./dot-commands/').filter(file => file.endsWith('.js')); + +// MySQL +const mysql = require('mysql'); +const db = new mysql.createConnection({ + host: dbHost, + user: dbUser, + password: dbPass, + database: dbName, + port: dbPort, +}); db.connect(); -module.exports = { - setValidExtensions(client) { - for (const entry of client.commands.map(command => command.name)) { - config.validExtensions.push(entry); - } - }, - getCommandFiles(client) { - if (!client.commands) client.commands = new Discord.Collection(); - client.commands.clear(); - for (const file of commandFiles) { - const command = require(`./commands/${file}`); - client.commands.set(command.name, command); - } - }, - getGifFiles(client) { - if (!client.gifs) client.gifs = new Discord.Collection(); - client.gifs.clear(); - const query = "SELECT name, embed_url FROM gifs"; - return new Promise((resolve, reject) => { - db.query(query) - .then(res => { - for (let row of res.rows) { - const gif = { - name: row.name, - embed_url: row.embed_url - }; - client.gifs.set(gif.name, gif); - } - resolve(); - }) - .catch(err => console.error(err)); - }); - }, - getPotPhrases(client) { - if (!client.potphrases) client.potphrases = new Discord.Collection(); - client.potphrases.clear(); - const query = "SELECT id, content FROM potphrases"; - db.query(query) - .then(res => { - for (let row of res.rows) { - const potphrase = { - id: row.id, - content: row.content - }; - client.potphrases.set(potphrase.id, potphrase); +const functions = { + collections: { + slashCommands(client) { + if (!client.slashCommands) client.slashCommands = new Discord.Collection(); + client.slashCommands.clear(); + for (const file of slashCommandFiles) { + const slashCommand = require(`./slash-commands/${file}`); + if (slashCommand.data != undefined) { + client.slashCommands.set(slashCommand.data.name, slashCommand); } - }) - .catch(err => console.error(err)); - }, - getPastaFiles(client) { - if (!client.pastas) client.pastas = new Discord.Collection(); - client.pastas.clear(); - const query = "SELECT name, content, iconurl FROM pastas"; - return new Promise((resolve, reject) => { - db.query(query) - .then(res => { - for (let row of res.rows) { - const pasta = { - name: row.name, - content: row.content, - iconUrl: row.iconurl - }; - client.pastas.set(pasta.name, pasta); - } - resolve(); - }) - .catch(err => console.error(err)); - }); - - }, - reload(client) { - this.getCommandFiles(client); - this.getGifFiles(client); - this.getPastaFiles(client); - this.getPotPhrases(client); - }, - getFileInfo(content) { - // Split the message content at the final instance of a period - const finalPeriod = content.lastIndexOf('.'); - if (finalPeriod < 0) return false; - const extension = content.slice(finalPeriod).replace('.','').toLowerCase(); - const filename = content.slice(0,finalPeriod).toLowerCase(); - const file = { - name: filename, - extension: extension - }; - return file; - }, - extIsValid(extension) { - const extensions = require('./config.json').validExtensions; - return extensions.includes(extension); - }, - cleanInput(string) { - return string.replace(/'/g, "''").replace(/\n/g, '\\n'); - }, - createGifEmbed(data, author, command) { - return new Discord.MessageEmbed() - .setAuthor('Command: ' + command) - .setImage(data.embed_url) - .setTimestamp() - .setFooter(`@${author.username}#${author.discriminator}`); - }, - saveGif(message, name, embed_url) { - const gif = { - name: name, - embed_url: embed_url - }; - message.client.gifs.set(gif.name, gif); - this.uploadGIF(name, embed_url); - }, - savePasta(message, name, content) { - const pasta = { - name: name, - content: content - }; - message.client.pastas.set(pasta.name, pasta); - - const query = `INSERT INTO pastas (name, content) VALUES ('${name}','${content}')`; - db.query(query); - - return "Success"; - }, - createAirportEmbed(data, author, command) { - const airport = data.airport[0]; - return new Discord.MessageEmbed() - .setAuthor('Command: ' + command) - .setTitle(airport.airport_name) - .addFields( - { name: 'Location', value: `${airport.city}, ${airport.state_abbrev}`, inline: true }, - { name: 'Coordinates', value: `${airport.latitude}, ${airport.longitude}`, inline: true }, - { name: 'Elevation', value: `${airport.elevation}ft`, inline: true }, - { name: 'More Information', value: airport.link_path } - ) - .setTimestamp() - .setFooter(`@${author.username}#${author.discriminator}`); - }, - createWeatherEmbed(data, author, command) { - const loc = data.location; - const weather = data.current; - return new Discord.MessageEmbed() - .setAuthor('Command: ' + command) - .setTitle(`${loc.name}, ${loc.region}, ${loc.country} Weather`) - .setDescription(`The weather is currently ${weather.condition.text}`) - .addFields( - { name: 'Temperature', value: `${weather.temp_f}°F (Feels like: ${weather.feelslike_f}°F)`, inline: true }, - { name: 'Winds', value: `${weather.wind_mph} ${weather.wind_dir}`, inline: true }, - { name: 'Pressure', value: `${weather.pressure_in}inHg`, inline: true }, - { name: 'Relative Humidity', value: `${weather.humidity}%`, inline: true } - ) - .setThumbnail(`https:${weather.condition.icon}`) - .setTimestamp() - .setFooter(`@${author.username}#${author.discriminator}`); - }, - textEmbed(content, author, command) { - return new Discord.MessageEmbed() - .setAuthor('Command: ' + command) - .setDescription(content) - .setTimestamp() - .setFooter(`@${author.username}#${author.discriminator}`); - }, - pastaEmbed(content, iconUrl, author) { - return new Discord.MessageEmbed() - .setAuthor('Command: ' + 'pasta') - .setDescription(content) - .setThumbnail(iconUrl) - .setTimestamp() - .setFooter(`@${author.username}#${author.discriminator}`); - }, - createStockEmbed(data, author, command) { - return new Discord.MessageEmbed() - .setAuthor('Command: ' + command) - .setTitle() - .setTimestamp() - .setFooter(`@${author.username}#${author.discriminator}`); - }, - createHelpEmbed(message) { - const { commands } = message.client; - let fields = []; - for (const entry of commands.map(command => [command.name, command.description, command.usage])) { - const name = entry[0]; - const description = entry[1]; - let usage; - if (entry[2] == undefined) { - usage = ''; - } else { - usage = entry[2]; } - const excludeList = [ - 'kill', - 'mapcommands', - 'newgif', - 'newpng', - 'oldgif', - 'strain', - 'stonk', - 'wrongbad' - ]; - if (excludeList.includes(name)) continue; - fields.push({ - name: name, - value: `${description}\n**Usage:** \`${usage}.${name}\`` + if (config.isDev) console.log('Slash Commands Collection Built'); + }, + setvalidCommands(client) { + for (const entry of client.dotCommands.map(command => command.name)) { + config.validCommands.push(entry); + } + if (config.isDev) console.log('Valid Commands Added to Config'); + }, + dotCommands(client) { + if (!client.dotCommands) client.dotCommands = new Discord.Collection(); + client.dotCommands.clear(); + for (const file of dotCommandFiles) { + const dotCommand = require(`./dot-commands/${file}`); + client.dotCommands.set(dotCommand.name, dotCommand); + } + if (config.isDev) console.log('Dot Commands Collection Built'); + }, + gifs(rows, client) { + if (!client.gifs) client.gifs = new Discord.Collection(); + client.gifs.clear(); + for (const row of rows) { + const gif = { + id: row.id, + name: row.name, + embed_url: row.embed_url + }; + client.gifs.set(gif.name, gif); + } + if (config.isDev) console.log('GIFs Collection Built'); + }, + joints(rows, client) { + if (!client.joints) client.joints = new Discord.Collection(); + client.joints.clear(); + for (const row of rows) { + const joint = { + id: row.id, + content: row.content + }; + client.joints.set(joint.id, joint); + } + if (config.isDev) console.log('Joints Collection Built'); + }, + pastas(rows, client) { + if (!client.pastas) client.pastas = new Discord.Collection(); + client.pastas.clear(); + for (const row of rows) { + const pasta = { + id: row.id, + name: row.name, + content: row.content, + iconUrl: row.iconurl, + }; + client.pastas.set(pasta.name, pasta); + } + if (config.isDev) console.log('Pastas Collection Built'); + }, + requests(rows, client) { + if (!client.requests) client.requests = new Discord.Collection(); + client.requests.clear(); + for (const row of rows) { + const request = { + id: row.id, + author: row.author, + request: row.request, + }; + client.requests.set(request.id, request); + } + if (config.isDev) console.log('Requests Collection Built'); + }, + strains(rows, client) { + if (!client.strains) client.strains = new Discord.Collection(); + client.strains.clear(); + for (const row of rows) { + const strain = { + id: row.id, + name: row.name, + }; + client.strains.set(strain.name, strain); + } + if (config.isDev) console.log('Strains Collection Built'); + } + }, + dot: { + getCommandData(message) { + const commandData = {}; + // Split the message content at the final instance of a period + const finalPeriod = message.content.lastIndexOf('.'); + if (finalPeriod < 0) { + commandData.isCommand = false; + return commandData; + } + commandData.isCommand = true; + commandData.args = message.content.slice(0,finalPeriod); + commandData.command = message.content.slice(finalPeriod).replace('.','').toLowerCase(); + commandData.author = `${message.author.username}#${message.author.discriminator}`; + return this.checkCommand(commandData); + }, + checkCommand(commandData) { + if (commandData.isCommand) { + const validCommands = require('./config.json').validCommands; + commandData.isValid = validCommands.includes(commandData.command); + } + else { + commandData.isValid = false; + console.error('Somehow a non-command made it to checkCommands()'); + } + return commandData; + } + }, + embeds: { + help(interaction) { + // Construct the Help Embed + const helpEmbed = new Discord.MessageEmbed() + .setColor('BLUE') + .setAuthor('Help Page') + .setDescription(strings.help.description) + .setThumbnail(strings.urls.avatar); + + // Construct the Slash Commands help + + let slashCommandsFields = []; + + const slashCommandsMap = interaction.client.slashCommands.map(e => { + return { + name: e.data.name, + description: e.data.description + }; + }) + + for (const e of slashCommandsMap) { + slashCommandsFields.push({ + name: `- /${e.name}`, + value: e.description, + inline: false, + }); + } + + // Construct the Dot Commands Help + let dotCommandsFields = []; + + const dotCommandsMap = interaction.client.dotCommands.map(e => { + return { + name: e.name, + description: e.description, + usage: e.usage + }; + }); + + for (const e of dotCommandsMap) { + dotCommandsFields.push({ + name: `- .${e.name}`, + value: `${e.description}\nUsage: ${e.usage}`, + inline: false, + }); + } + + helpEmbed.addField('Slash Commands', strings.help.slash); + helpEmbed.addFields(slashCommandsFields); + helpEmbed.addField('Dot Commands', strings.help.dot); + helpEmbed.addFields(dotCommandsFields); + + return { embeds: [ + helpEmbed + ]}; + }, + gif(commandData) { + return { embeds: [new Discord.MessageEmbed() + .setAuthor(`${commandData.args}.${commandData.command}`) + .setImage(commandData.embed_url) + .setTimestamp() + .setFooter(commandData.author)]}; + }, + pasta(commandData) { + return { embeds: [ new Discord.MessageEmbed() + .setAuthor(`${commandData.args}.${commandData.command}`) + .setDescription(commandData.content) + .setThumbnail(commandData.iconUrl) + .setTimestamp() + .setFooter(commandData.author)]}; + }, + pastas(commandData) { + const pastasArray = []; + const pastasEmbed = new Discord.MessageEmbed() + .setAuthor(commandData.command) + .setTimestamp() + .setFooter(commandData.author); + + for (const row of commandData.pastas) { + pastasArray.push(`#${row.id} - ${row.name}.pasta`); + } + + const pastasString = pastasArray.join('\n'); + pastasEmbed.setDescription(pastasString); + + return { embeds: [pastasEmbed] }; + }, + gifs(commandData) { + const gifsArray = []; + const gifsEmbed = new Discord.MessageEmbed() + .setAuthor(commandData.command) + .setTimestamp() + .setFooter(commandData.author); + + for (const row of commandData.gifs) { + gifsArray.push(`#${row.id} - ${row.name}.gif`); + } + + const gifsString = gifsArray.join('\n'); + gifsEmbed.setDescription(gifsString); + + return { embeds: [gifsEmbed] }; + }, + text(commandData) { + return { embeds: [new Discord.MessageEmbed() + .setAuthor(commandData.command) + .setDescription(commandData.content) + .setTimestamp() + .setFooter(commandData.author)]}; + }, + requests(commandData) { + const requestsEmbed = new Discord.MessageEmbed() + .setAuthor(commandData.command) + .setTimestamp() + .setFooter(commandData.author); + + const requestsArray = []; + + for (const row of commandData.requests) { + requestsArray.push( + `**#${row.id} - ${row.author}**`, + `Request: ${row.request}` + ); + } + + requestsEmbed.setDescription(requestsArray.join('\n')); + + return { embeds: [requestsEmbed]}; + }, + strain(commandData, message) { + const strainEmbed = new Discord.MessageEmbed() + .setAuthor(`${commandData.command} #${commandData.strainInfo.id}`) + .setTimestamp() + .setFooter(commandData.author); + const { strainInfo } = commandData; + strainEmbed.addFields([ + { + name: 'Strain Name', + value: `${strainInfo.name}`, + }, + { + name: 'Type', + value: `${strainInfo.type}`, + inline: true, + }, + { + name: 'Effects', + value: `${strainInfo.effects}`, + inline: true, + }, + { + name: 'Treats', + value: `${strainInfo.ailments}`, + inline: true, + }, + { + name: 'Flavor', + value: `${strainInfo.flavor}`, + inline: true, + }, + ]); + + message.reply({ embeds: [ strainEmbed ]}); + }, + }, + collect: { + gifName(interaction) { + const gifNameFilter = m => m.author.id == strings.temp.gifUserId; + return interaction.channel.createMessageCollector({ filter: gifNameFilter, time: 30000 }); + }, + }, + upload: { + request(commandData, client) { + const query = `INSERT INTO requests (author, request, status) VALUES ('${commandData.author}','${commandData.args}','Active')`; + db.query(query, (err, rows, fields) => { + if (err) throw err; + functions.download.requests(client); + }); + }, + pasta(pastaData, client) { + const query = `INSERT INTO pastas (name, content) VALUES ('${pastaData.name}','${pastaData.content}')`; + db.query(query, (err, rows, fields) => { + if (err) throw err; + functions.download.pastas(client); + }); + }, + joint(content, client) { + const query = `INSERT INTO joints (content) VALUES ('${content}')`; + db.query(query, (err, rows, fields) => { + if (err) throw err; + functions.download.joints(client); + }); + }, + gif(gifData, client) { + const query = `INSERT INTO gifs (name, embed_url) VALUES ('${gifData.name}', '${gifData.embed_url}')`; + db.query(query, (err, rows, fields) => { + if (err) throw err; + functions.download.gifs(client); }); } - - return new Discord.MessageEmbed() - .setAuthor('NodBot Help') - .setDescription('All commands are provided as "file extensions" instead of prefixes to the message.') - .addFields(fields) - .setTimestamp(); }, - createGIFList(message) { - let list = []; - const { gifs } = message.client; - for (const entry of gifs.map(gif => [gif.name])) { - list.push(entry[0] + '.gif'); - } - - return new Discord.MessageEmbed() - .setAuthor('NodBot GIF List') - .setTitle('List of Currently Saved GIFs') - .setDescription(list.join('\n')) - .setTimestamp() - .setFooter(`@${message.author.username}#${message.author.discriminator}`); - }, - createPastaList(message) { - let list = []; - const { pastas } = message.client; - for (const entry of pastas.map(pasta => [pasta.name])) { - list.push(entry[0] + '.pasta'); - } - - return new Discord.MessageEmbed() - .setAuthor('NodBot Pasta List') - .setTitle('List of Currently Saved Copypastas') - .setDescription(list.join('\n')) - .setTimestamp() - .setFooter(`@${message.author.username}#${message.author.discriminator}`); - }, - uploadGIF(name, embed_url) { - const query = `INSERT INTO gifs (name, embed_url) VALUES ('${name}','${embed_url}')`; - db.query(query) - .then() - .catch(e => console.error(e)); - }, - uploadPotPhrase(content) { - const query = `INSERT INTO potphrases (content) VALUES ('${content}')`; - db.query(query) - .then() - .catch(e => console.error(e)); - }, - uploadRequest(author, request) { - const query = `INSERT INTO requests (author, request, status) VALUES ('@${author.username}#${author.discriminator}','${request}','Active')`; - db.query(query) - .then() - .catch(e => console.error(e)); - }, - getActiveRequests(message) { - const query = "SELECT * FROM requests WHERE status = 'Active'"; - let rows; - db.query(query) - .then(res => { - const embed = this.requestsEmbed(res.rows); - message.channel.send(embed); - }) - .catch(e => console.error(e)); - return rows; - }, - requestsEmbed(rows) { - let fields = []; - for (const row of rows) { - fields.push({ - name: '#' + row.id, - value: row.request + `\nSubmitted by ${row.author}` + download: { + requests(client) { + const query = 'SELECT * FROM requests WHERE status = \'Active\' ORDER BY id ASC'; + db.query(query, (err, rows, fields) => { + if (err) throw err; + functions.collections.requests(rows, client); }); - } - - return new Discord.MessageEmbed() - .setAuthor('NodBot Requests') - .setTitle('Currently Active Requests') - .addFields(fields) - .setTimestamp(); + }, + pastas(client) { + const query = 'SELECT * FROM pastas ORDER BY id ASC'; + db.query(query, (err, rows, fields) => { + if (err) throw err; + functions.collections.pastas(rows, client); + }); + }, + gifs(client) { + const query = 'SELECT * FROM gifs ORDER BY id ASC'; + db.query(query, (err, rows, fields) => { + if (err) throw err; + functions.collections.gifs(rows, client); + }); + }, + joints(client) { + const query = 'SELECT * FROM joints ORDER BY id ASC'; + db.query(query, (err, rows, fields) => { + if (err) throw err; + functions.collections.joints(rows, client); + }); + }, + strain(commandData, message) { + const { strainName } = commandData; + const query = `SELECT id, name, type, effects, ailment, flavor FROM strains WHERE name = '${strainName}'`; + db.query(query, (err, rows, fields) => { + if (rows != undefined) { + commandData.strainInfo = { + id: `${rows[0].id}`, + name: `${rows[0].name}`, + type: `${rows[0].type}`, + effects: `${rows[0].effects}`, + ailments: `${rows[0].ailment}`, + flavor: `${rows[0].flavor}`, + }; + functions.embeds.strain(commandData, message); + } + }); + }, + strains(client) { + const query = 'SELECT id, name FROM strains'; + db.query(query, (err, rows, fields) => { + if (err) throw err; + functions.collections.strains(rows, client); + }); + }, }, - closeRequest(id) { - const query = `UPDATE requests SET status = 'Closed' WHERE id = ${id}`; - db.query(query) - .then() - .catch(e => console.error(e)); - } -} \ No newline at end of file + weed: { + strain: { + lookup(strainName, client) { + const strainSearcher = new FuzzySearch(client.strains.map(e => e.name)); + const name = strainSearcher.search(strainName)[0]; + if (name != undefined) { + return name; + } else { + return false; + } + }, + submit(strainName) { + return strainName; + } + } + }, + // Parent-Level functions (miscellaneuous) + closeRequest(requestId, client) { + const query = `UPDATE requests SET status = 'Closed' WHERE id = ${requestId}`; + db.query(query, (err, rows, fields) => { + if (err) throw err; + functions.download.requests(client); + }); + }, +}; + +module.exports = functions; \ No newline at end of file diff --git a/index.js b/index.js deleted file mode 100644 index 1cb81f7..0000000 --- a/index.js +++ /dev/null @@ -1,72 +0,0 @@ -// Variable Assignment -// Environment Variable Setup -const dotenv = require('dotenv'); -dotenv.config(); -// Discord.js library -const Discord = require('discord.js'); -// Create the client -const client = new Discord.Client(); -// External Functions File -const functions = require('./functions.js'); - -// Once the client is logged in and ready -client.once('ready', () => { - console.log('Ready'); - // This sets the activity that shows below the bot's name in the member/friend list - client.user.setActivity('Nod Simulator 2021', { type: 'PLAYING' }).then().catch(console.error); - // Import the Command, GIF, and Pasta files into collections - functions.getCommandFiles(client); - functions.getGifFiles(client); - functions.getPastaFiles(client); - functions.getPotPhrases(client); - functions.setValidExtensions(client); - // Get the owner and DM them a message that the bot is ready, useful for remote deployment - client.users.fetch(process.env.ownerID).then(user => { - user.createDM().then(channel => { - channel.send('Ready'); - }); - }); -}); - -// Log into discord using the TOKEN provided by environment variables (.env) -client.login(process.env.TOKEN) - .then() - .catch(err => { - console.error(err); - // Dump the TOKEN into the console as the TOKEN is likely the cause of any error logging in, unless Discord servers are down. - console.log('Token: ' + process.env.TOKEN) - }); - -// This runs on each message the bot sees -client.on('message', message => { - // Out here smoking big doinks in Amish - if ((message.content.toLowerCase().includes('big')) && message.content.toLowerCase().includes('doinks')) { - message.channel.send('gang.'); - } - - // Get the filename and extension as an array - const file = functions.getFileInfo(message.content); - if (!file) return; - - - - // If the message is from a bot, or doesn't have a valid file extension, stop here. - if (functions.extIsValid(file.extension) == false || message.author.bot) return; - - // If the command collection doesn't contain the given command, stop here. - if (!client.commands.has(file.extension)) return; - - try { - // Attempt to execute the command - client.commands.get(file.extension).execute(message, file); - } catch (error) { - // Log errors and let the user know something went wrong. - console.error(error); - message.channel.send('There was an error trying to execute that command.'); - } - - // Try to delete the requester's message - if (message.deletable) { - message.delete().then().catch(err => console.error(err)); - } -}); \ No newline at end of file diff --git a/list-of-commands.txt b/list-of-commands.txt index 910f7bf..6ac6c54 100644 --- a/list-of-commands.txt +++ b/list-of-commands.txt @@ -1,25 +1,23 @@ -Slash Commands: -/closereq -/gifs -/help -/joint -/joints -/lenny -/pastas -/ping -/requests -/savejoint (prev. .roll) +Slash Commands: [* means updated] +*/closereq* +*/gifs +*/help* +*/joint* +*/joints* +*/lenny* +*/pastas* +*/ping* +*/requests* +*/savejoint (prev. .roll) /savegif -/savepasta -/sendhelp -/truth +*/savepasta +*/truth TODO: In the future I'd like to change to a single `/save {type}` command -Dot-Extension Commands: -.gif -.pasta -.request -.spongebob -.strain -.weather \ No newline at end of file +Dot-Extension Commands: [* means updated] +*.gif +*.pasta +*.request +*.spongebob +*.strain \ No newline at end of file diff --git a/main.js b/main.js index e69de29..b4e28c6 100644 --- a/main.js +++ b/main.js @@ -0,0 +1,204 @@ +/* eslint-disable no-case-declarations */ +/* eslint-disable indent */ +// dotenv for handling environment variables +const dotenv = require('dotenv'); +dotenv.config(); +const token = process.env.TOKEN; + +// Discord.JS +const { Client, Intents } = require('discord.js'); +const client = new Client({ + intents: [ + 'GUILDS', + 'GUILD_MESSAGES', + 'GUILD_MESSAGE_REACTIONS', + 'DIRECT_MESSAGES', + 'DIRECT_MESSAGE_REACTIONS', + ], + partials: [ + 'CHANNEL', + 'MESSAGE', + ], +}); +const { MessageActionRow, MessageButton } = require('discord.js'); + +// Various imports +const fn = require('./functions.js'); +const config = require('./config.json'); +const strings = require('./strings.json'); + +client.once('ready', () => { + fn.collections.slashCommands(client); + fn.collections.dotCommands(client); + fn.collections.setvalidCommands(client); + fn.download.gifs(client); + fn.download.pastas(client); + fn.download.joints(client); + fn.download.requests(client); + fn.download.strains(client); + console.log('Ready!'); + client.channels.fetch(config.devChannelId).then(channel => { + channel.send(`I'm ready! ${new Date().toISOString()}`); + }); +}); + +// slash-commands +client.on('interactionCreate', async interaction => { + if (interaction.isCommand()) { + if (config.isDev) { + console.log(interaction); + } + const { commandName } = interaction; + + if (client.slashCommands.has(commandName)) { + client.slashCommands.get(commandName).execute(interaction); + } else { + interaction.reply('Sorry, I don\'t have access to that command.'); + console.error('Slash command attempted to run but not found: ' + commandName); + } + } + + if (interaction.isButton()) { + if (interaction.user.id != strings.temp.gifUserId) return; + // Get some meta info from strings + const index = strings.temp.gifIndex; + const limit = strings.temp.gifLimit; + let newIndex; + const buttonId = interaction.component.customId; + switch (buttonId) { + case 'prevGif': + newIndex = index - 1; + strings.temp.gifIndex = newIndex; + // If we're leaving the last GIF, enable the Next GIF button + if (index == limit) { + // Re-Send Previous GIF button + const prevButton = new MessageButton().setCustomId('prevGif').setLabel('Previous GIF').setStyle('SECONDARY'); + // Re-Send Confirm GIF Button + const confirmButton = new MessageButton().setCustomId('confirmGif').setLabel('Confirm').setStyle('PRIMARY'); + // Enable Next GIF Button + const nextButton = new MessageButton().setCustomId('nextGif').setLabel('Next GIF').setStyle('SECONDARY'); + // Re-Send Cancel Button + const cancelButton = new MessageButton().setCustomId('cancelGif').setLabel('Cancel').setStyle('DANGER'); + // Put all the above into an ActionRow to be sent as a component of the reply + const row = new MessageActionRow().addComponents(prevButton, confirmButton, nextButton, cancelButton); + + interaction.update({ content: strings.temp.gifs[newIndex].embed_url, components: [row] }); + break; + } + // If we're going into the first GIF, disable the Previous GIF button + if (newIndex == 0) { + // Disable Previous GIF button + const prevButton = new MessageButton().setCustomId('prevGif').setLabel('Previous GIF').setStyle('SECONDARY').setDisabled(); + // Re-Send Confirm GIF Button + const confirmButton = new MessageButton().setCustomId('confirmGif').setLabel('Confirm').setStyle('PRIMARY'); + // Re-Send Next GIF Button + const nextButton = new MessageButton().setCustomId('nextGif').setLabel('Next GIF').setStyle('SECONDARY'); + // Re-Send Cancel Button + const cancelButton = new MessageButton().setCustomId('cancelGif').setLabel('Canceled').setStyle('DANGER'); + // Put all the above into an ActionRow to be sent as a component of the reply + const row = new MessageActionRow().addComponents(prevButton, confirmButton, nextButton, cancelButton); + + interaction.update({ content: strings.temp.gifs[newIndex].embed_url, components: [row] }); + break; + } + + interaction.update(strings.temp.gifs[newIndex].embed_url); + break; + case 'confirmGif': + interaction.update({ content: 'GIF Confirmed, what should I save it as?\n(*don\'t* include the .gif)', components: [] }); + const collector = fn.collect.gifName(interaction); + collector.on('collect', m => { + const gifData = { + name: m.content.toLowerCase(), + embed_url: strings.temp.gifs[strings.temp.gifIndex].embed_url, + }; + fn.upload.gif(gifData, client); + m.reply(`I've saved the GIF as ${gifData.name}.gif`); + fn.download.gifs(interaction.client); + collector.stop('success'); + }); + fn.download.gifs(interaction.client); + break; + case 'nextGif': + newIndex = index + 1; + strings.temp.gifIndex = newIndex; + // If we're leaving the first GIF, enable the Previous GIF button + if (index == 0) { + // Enable Previous GIF button + const prevButton = new MessageButton().setCustomId('prevGif').setLabel('Previous GIF').setStyle('SECONDARY').setDisabled(false); + // Re-Send Confirm GIF Button + const confirmButton = new MessageButton().setCustomId('confirmGif').setLabel('Confirm').setStyle('PRIMARY'); + // Re-Send Next GIF Button + const nextButton = new MessageButton().setCustomId('nextGif').setLabel('Next GIF').setStyle('SECONDARY'); + // Re-Send Cancel Button + const cancelButton = new MessageButton().setCustomId('cancelGif').setLabel('Cancel').setStyle('DANGER'); + // Put all the above into an ActionRow to be sent as a component of the reply + const row = new MessageActionRow().addComponents(prevButton, confirmButton, nextButton, cancelButton); + + interaction.update({ content: strings.temp.gifs[newIndex].embed_url, components: [row] }); + break; + } + // If we're going into the last GIF, disable the Next GIF button + if (newIndex == strings.temp.gifLimit) { + // Re-Send Previous GIF button + const prevButton = new MessageButton().setCustomId('prevGif').setLabel('Previous GIF').setStyle('SECONDARY'); + // Re-Send Confirm GIF Button + const confirmButton = new MessageButton().setCustomId('confirmGif').setLabel('Confirm').setStyle('PRIMARY'); + // Disable Next GIF Button + const nextButton = new MessageButton().setCustomId('nextGif').setLabel('Next GIF').setStyle('SECONDARY').setDisabled(); + // Re-Send Cancel Button + const cancelButton = new MessageButton().setCustomId('cancelGif').setLabel('Canceled').setStyle('DANGER'); + // Put all the above into an ActionRow to be sent as a component of the reply + const row = new MessageActionRow().addComponents(prevButton, confirmButton, nextButton, cancelButton); + + interaction.update({ content: strings.temp.gifs[newIndex].embed_url, components: [row] }); + break; + } + + interaction.update(strings.temp.gifs[newIndex].embed_url); + break; + case 'cancelGif': + // Previous GIF button + const prevButton = new MessageButton().setCustomId('prevGif').setLabel('Previous GIF').setStyle('SECONDARY').setDisabled(); + // Confirm GIF Button + const confirmButton = new MessageButton().setCustomId('confirmGif').setLabel('Confirm').setStyle('PRIMARY').setDisabled(); + // Next GIF Button + const nextButton = new MessageButton().setCustomId('nextGif').setLabel('Next GIF').setStyle('SECONDARY').setDisabled(); + // Cancel Button + const cancelButton = new MessageButton().setCustomId('cancelGif').setLabel('Canceled').setStyle('DANGER'); + // Put all the above into an ActionRow to be sent as a component of the reply + const row = new MessageActionRow().addComponents(prevButton, confirmButton, nextButton, cancelButton); + interaction.component.setDisabled(true); + + interaction.update({ content: 'Canceled.', components: [row] }); + break; + default: + break; + } + } +}); + +// dot-commands +client.on('messageCreate', message => { + // Some basic checking to prevent running unnecessary code + if (message.author.bot) return; + + // Wildcard Responses, will respond if any message contains the trigger word(s), excluding self-messages + if (message.content.includes('big') && message.content.includes('doinks')) message.reply('gang.'); + if (message.content.includes('ligma')) message.reply('ligma balls, goteem'); + + const commandData = fn.dot.getCommandData(message); + console.log(commandData); + if (commandData.isValid && commandData.isCommand) { + try { + client.dotCommands.get(commandData.command).execute(message, commandData); + } + catch (error) { + console.error(error); + message.reply('There was an error trying to execute that command.'); + } + } + return; +}); + +client.login(token); \ No newline at end of file diff --git a/package.json b/package.json index 504a86a..1648521 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "discord-api-types": "^0.22.0", "discord.js": "^13.1.0", "dotenv": "^10.0.0", - "pg": "^8.6.0", + "fuzzy-search": "^3.2.1", + "mysql": "^2.18.1", "tenorjs": "^1.0.8" }, "devDependencies": { diff --git a/slash-commands/closereq.js b/slash-commands/closereq.js new file mode 100644 index 0000000..6d4aaa8 --- /dev/null +++ b/slash-commands/closereq.js @@ -0,0 +1,17 @@ +const { SlashCommandBuilder } = require('@discordjs/builders'); +const fn = require('../functions.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('closereq') + .setDescription('Close a request by ID, retrieved from /requests') + .addStringOption(option => + option.setName('requestid') + .setDescription('The ID of the request you\'d like to close.') + .setRequired(true)), + async execute(interaction) { + const requestId = interaction.options.getString('requestid'); + fn.closeRequest(requestId, interaction.client); + interaction.reply(`Request #${requestId} closed.`); + }, +}; \ No newline at end of file diff --git a/slash-commands/gifs.js b/slash-commands/gifs.js new file mode 100644 index 0000000..934ff86 --- /dev/null +++ b/slash-commands/gifs.js @@ -0,0 +1,33 @@ +const { SlashCommandBuilder } = require('@discordjs/builders'); +const { config } = require('dotenv'); +const fn = require('../functions.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('gifs') + .setDescription('Get a list of currently saved GIFs.'), + async execute(interaction) { + if (!interaction.client.gifs) { + interaction.reply('For some reason I don\'t have access to the collection of gifs. Sorry about that!'); + return; + } + const gifsMap = interaction.client.gifs.map(e => { + return { + id: e.id, + name: e.name, + }; + }); + const commandData = { + gifs: [], + command: 'gifs', + author: interaction.user.tag, + }; + for (const row of gifsMap) { + commandData.gifs.push({ + id: row.id, + name: row.name, + }); + } + interaction.reply(fn.embeds.gifs(commandData)); + }, +}; \ No newline at end of file diff --git a/slash-commands/help.js b/slash-commands/help.js new file mode 100644 index 0000000..d424e6b --- /dev/null +++ b/slash-commands/help.js @@ -0,0 +1,30 @@ +const { SlashCommandBuilder } = require('@discordjs/builders'); +const fn = require('../functions.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('help') + .setDescription('Send the help page.') + .addStringOption(option => + option.setName('location') + .setDescription('Send help in this channel or in DMs?') + .setRequired(true) + .addChoice('Here', 'channel') + .addChoice('DMs', 'dm')), + async execute(interaction) { + switch (interaction.options.getString('location')) { + case 'channel': + await interaction.reply(fn.embeds.help(interaction)); + break; + case 'dm': + await interaction.user.createDM().then(channel => { + channel.send(fn.embeds.help(interaction)); + interaction.reply('I\'ve sent you a copy of my help page.'); + }); + break; + default: + interaction.reply('There was an error, please try again.'); + break; + } + }, +}; \ No newline at end of file diff --git a/slash-commands/joint.js b/slash-commands/joint.js new file mode 100644 index 0000000..9bd6f92 --- /dev/null +++ b/slash-commands/joint.js @@ -0,0 +1,17 @@ +const { SlashCommandBuilder } = require('@discordjs/builders'); +const fn = require('../functions.js'); +const { emoji } = require('../strings.json'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('joint') + .setDescription('Replies with a random cannabis-related quote.'), + async execute(interaction) { + let joints = []; + for (const entry of interaction.client.joints.map(joint => joint.content)) { + joints.push(entry); + } + const randIndex = Math.floor(Math.random() * joints.length); + interaction.reply(`${joints[randIndex]} ${emoji.joint}`); + }, +}; \ No newline at end of file diff --git a/slash-commands/joints.js b/slash-commands/joints.js new file mode 100644 index 0000000..98d5d2f --- /dev/null +++ b/slash-commands/joints.js @@ -0,0 +1,15 @@ +const { SlashCommandBuilder } = require('@discordjs/builders'); +const fn = require('../functions.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('joints') + .setDescription('Send a list of all the /joint phrases.'), + async execute(interaction) { + let joints = []; + interaction.client.joints.map(e => { + joints.push(e.content); + }); + interaction.reply('Here are all the `.joint` phrases I have saved:\n\n' + joints.join('\n')); + }, +}; \ No newline at end of file diff --git a/slash-commands/lenny.js b/slash-commands/lenny.js new file mode 100644 index 0000000..942e920 --- /dev/null +++ b/slash-commands/lenny.js @@ -0,0 +1,11 @@ +const { SlashCommandBuilder } = require('@discordjs/builders'); +const fn = require('../functions.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('lenny') + .setDescription('( ͡° ͜ʖ ͡°)'), + async execute(interaction) { + await interaction.channel.send('( ͡° ͜ʖ ͡°)'); + }, +}; \ No newline at end of file diff --git a/slash-commands/pastas.js b/slash-commands/pastas.js new file mode 100644 index 0000000..2736df1 --- /dev/null +++ b/slash-commands/pastas.js @@ -0,0 +1,33 @@ +const { SlashCommandBuilder } = require('@discordjs/builders'); +const { config } = require('dotenv'); +const fn = require('../functions.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('pastas') + .setDescription('Get a list of currently saved copypastas.'), + async execute(interaction) { + if (!interaction.client.pastas) { + interaction.reply('For some reason I don\'t have access to the collection of copypastas. Sorry about that!'); + return; + } + const commandData = { + author: interaction.user.tag, + command: interaction.commandName, + pastas: [], + }; + const pastasMap = interaction.client.pastas.map(e => { + return { + id: e.id, + name: e.name, + }; + }); + for (const row of pastasMap) { + commandData.pastas.push({ + id: row.id, + name: row.name, + }); + } + interaction.reply(fn.embeds.pastas(commandData)); + }, +}; \ No newline at end of file diff --git a/slash-commands/ping.js b/slash-commands/ping.js new file mode 100644 index 0000000..3bb70f1 --- /dev/null +++ b/slash-commands/ping.js @@ -0,0 +1,11 @@ +const { SlashCommandBuilder } = require('@discordjs/builders'); +const fn = require('../functions.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('ping') + .setDescription('Check that the bot is alive and responding.'), + async execute(interaction) { + await interaction.reply('Pong!'); + }, +}; \ No newline at end of file diff --git a/slash-commands/reload.js b/slash-commands/reload.js new file mode 100644 index 0000000..1b9c3f3 --- /dev/null +++ b/slash-commands/reload.js @@ -0,0 +1,20 @@ +const { SlashCommandBuilder } = require('@discordjs/builders'); +const fn = require('../functions.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('reload') + .setDescription('Reload all saved content, useful if saving something fails.'), + async execute(interaction) { + const { client } = interaction; + fn.collections.slashCommands(client); + fn.collections.dotCommands(client); + fn.collections.setvalidCommands(client); + fn.download.gifs(client); + fn.download.pastas(client); + fn.download.joints(client); + fn.download.requests(client); + fn.download.strains(client); + interaction.reply('Reloaded!'); + }, +}; \ No newline at end of file diff --git a/slash-commands/requests.js b/slash-commands/requests.js new file mode 100644 index 0000000..75caa80 --- /dev/null +++ b/slash-commands/requests.js @@ -0,0 +1,31 @@ +const { SlashCommandBuilder } = require('@discordjs/builders'); +const { config } = require('dotenv'); +const fn = require('../functions.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('requests') + .setDescription('Get a list of Active requests from the database'), + async execute(interaction) { + const commandData = { + author: interaction.user.tag, + command: interaction.commandName, + requests: [], + }; + const requestsMap = interaction.client.requests.map(e => { + return { + id: e.id, + author: e.author, + request: e.request, + }; + }); + for (const row of requestsMap) { + commandData.requests.push({ + id: row.id, + author: row.author, + request: row.request, + }); + } + interaction.reply(fn.embeds.requests(commandData)); + }, +}; \ No newline at end of file diff --git a/slash-commands/savegif.js b/slash-commands/savegif.js new file mode 100644 index 0000000..1413b7c --- /dev/null +++ b/slash-commands/savegif.js @@ -0,0 +1,56 @@ +const tenor = require('tenorjs').client({ + 'Key': process.env.tenorAPIKey, // https://tenor.com/developer/keyregistration + 'Filter': 'off', // "off", "low", "medium", "high", not case sensitive + 'Locale': 'en_US', + 'MediaFilter': 'minimal', + 'DateFormat': 'D/MM/YYYY - H:mm:ss A', +}); + +const { SlashCommandBuilder } = require('@discordjs/builders'); +const { MessageActionRow, MessageButton } = require('discord.js'); +const fn = require('../functions.js'); +const strings = require('../strings.json'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('savegif') + .setDescription('Search Tenor for a GIF to save') + .addStringOption(option => + option.setName('query') + .setDescription('Search Query') + .setRequired(true)), + async execute(interaction) { + // Previous GIF button + const prevButton = new MessageButton().setCustomId('prevGif').setLabel('Previous GIF').setStyle('SECONDARY').setDisabled(true); + // Confirm GIF Button + const confirmButton = new MessageButton().setCustomId('confirmGif').setLabel('Confirm').setStyle('PRIMARY'); + // Next GIF Button + const nextButton = new MessageButton().setCustomId('nextGif').setLabel('Next GIF').setStyle('SECONDARY'); + // Cancel Button + const cancelButton = new MessageButton().setCustomId('cancelGif').setLabel('Cancel').setStyle('DANGER'); + // Put all the above into an ActionRow to be sent as a component of the reply + const actionRow = new MessageActionRow().addComponents(prevButton, confirmButton, nextButton, cancelButton); + + // Get the query + const query = interaction.options.getString('query'); + + // Search Tenor for the GIF + tenor.Search.Query(query, '10').then(res => { + strings.temp.gifs = []; + strings.temp.gifIndex = 0; + strings.temp.gifLimit = res.length - 1; + strings.temp.gifUserId = interaction.user.id; + + if (res[0] == undefined) { + interaction.reply('Sorry I was unable to find a GIF of ' + query); + return; + } + for (const row of res) { + strings.temp.gifs.push({ + embed_url: row.media[0].gif.url, + }); + } + interaction.reply({ content: strings.temp.gifs[0].embed_url, components: [actionRow] }); + }); + }, +}; \ No newline at end of file diff --git a/slash-commands/savejoint.js b/slash-commands/savejoint.js new file mode 100644 index 0000000..43e0f67 --- /dev/null +++ b/slash-commands/savejoint.js @@ -0,0 +1,17 @@ +const { SlashCommandBuilder } = require('@discordjs/builders'); +const fn = require('../functions.js'); +const { emoji } = require('../strings.json'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('savejoint') + .setDescription('Save a phrase for /joint!') + .addStringOption(option => + option.setName('joint-content') + .setDescription('What is the phrase?') + .setRequired(true)), + async execute(interaction) { + fn.upload.joint(interaction.options.getString('joint-content'), interaction.client); + interaction.reply(`The joint has been rolled${emoji.joint}`); + }, +}; \ No newline at end of file diff --git a/slash-commands/savepasta.js b/slash-commands/savepasta.js new file mode 100644 index 0000000..4776561 --- /dev/null +++ b/slash-commands/savepasta.js @@ -0,0 +1,24 @@ +const { SlashCommandBuilder } = require('@discordjs/builders'); +const fn = require('../functions.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('savepasta') + .setDescription('Save a copypasta!') + .addStringOption(option => + option.setName('pasta-name') + .setDescription('What should the name of the copypasta be?') + .setRequired(true)) + .addStringOption(option => + option.setName('pasta-content') + .setDescription('What is the content of the copypasta?') + .setRequired(true)), + async execute(interaction) { + const pastaData = { + name: interaction.options.getString('pasta-name'), + content: interaction.options.getString('pasta-content'), + }; + fn.upload.pasta(pastaData, interaction.client); + interaction.reply(`The copypasta has been saved as ${pastaData.name}.pasta`); + }, +}; \ No newline at end of file diff --git a/slash-commands/template b/slash-commands/template new file mode 100644 index 0000000..c214fae --- /dev/null +++ b/slash-commands/template @@ -0,0 +1,11 @@ +const { SlashCommandBuilder } = require('@discordjs/builders'); +const fn = require('../functions.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('') + .setDescription(''), + async execute(interaction) { + await + }, +}; \ No newline at end of file diff --git a/slash-commands/truth.js b/slash-commands/truth.js new file mode 100644 index 0000000..9107844 --- /dev/null +++ b/slash-commands/truth.js @@ -0,0 +1,11 @@ +const { SlashCommandBuilder } = require('@discordjs/builders'); +const fn = require('../functions.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('truth') + .setDescription('The truth about the MHallihan Flight Simulator'), + async execute(interaction) { + await interaction.reply('https://www.twitch.tv/hochmania/clip/EsteemedSlickDootStinkyCheese-hncmP8aIP8_WQb_a?filter=clips&range=all&sort=time'); + }, +}; \ No newline at end of file diff --git a/src/strings.json b/src/strings.json deleted file mode 100644 index 38fb002..0000000 --- a/src/strings.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "weed": [ - "It's dangerous to go alone, take this", - "Dave's not here, man", - "I was gonna clean my room, but then I got high", - "When in doubt, smoke it out", - "I smoke two joints before I smoke two joints, and then I smoke two more", - "Today's forecast is cloudy with a chance of munchies", - "You're gonna be doing a lot of doobie rollin' when you're living in a ran down by the river!", - "You'll have plenty of time to live in a van down by the river... when you're living in a van down by the river!", - "Roll, roll, roll my joint, pick out the seeds and stems" - ], - "emoji": { - "joint": "<:joint:862082955902976000>", - "next": "⏭️", - "previous": "⏮️", - "confirm": "☑️", - "cancel": "❌" - } -} \ No newline at end of file diff --git a/strings.json b/strings.json new file mode 100644 index 0000000..ccc342e --- /dev/null +++ b/strings.json @@ -0,0 +1,18 @@ +{ + "help": { + "description": "Hi there! Thanks for checking out NodBot. NodBot is used in two distinct ways: with 'Slash Commands' (/help), and with 'Dot Commands' (nod.gif). The two types will be outlined below, along with usage examples.", + "slash": "Slash Commands always begin with a / and a menu will pop up to help complete the commands.", + "dot": "Dot Commands have the command at the end of the message, for example to search for a gif of 'nod', type 'nod.gif'" + }, + "emoji": { + "joint": "<:joint:862082955902976000>", + "next": "⏭️", + "previous": "⏮️", + "confirm": "☑️", + "cancel": "❌" + }, + "urls": { + "avatar": "https://cdn.discordapp.com/avatars/513184762073055252/12227aa23a06d5178853e59b72c7487b.webp?size=128" + }, + "temp": {} +} \ No newline at end of file diff --git a/testing.txt b/testing.txt new file mode 100644 index 0000000..38cd69f --- /dev/null +++ b/testing.txt @@ -0,0 +1,23 @@ +[ NodBot v3 Testing Procedure ] + +/ping +/help [Here | DMs] +/gifs +/pastas +/joints +/requests +/savegif +/savepasta +/savejoint +[ ! CREATE MANUAL ENTRIES IN ALL 4 DBs ! ] +/reload + +/joint +/truth + +nod.gif +random.gif +bush.pasta +random.request + +[ TODO: Error handling testing ] \ No newline at end of file