diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 5008ddf..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.gitea/workflows/pe-docker.yml b/.gitea/workflows/pe-docker.yml index dc938c7..9e13c26 100644 --- a/.gitea/workflows/pe-docker.yml +++ b/.gitea/workflows/pe-docker.yml @@ -10,15 +10,12 @@ env: DHUB_PWORD: ${{ secrets.DHUB_PWORD }} jobs: - build: - runs-on: self-hosted - steps: - name: Pull latest from Git run: | - echo "${{ gitea.ref_name }}" + echo "Branch: ${{ gitea.head_ref }}" pwd whoami mkdir -p /var/lib/act_runner/ @@ -30,7 +27,7 @@ jobs: cd nodbot git pull fi - git checkout ${{ gitea.ref_name }} + git checkout ${{ gitea.head_ref }} - name: Build the Docker image run: | cd /var/lib/act_runner/nodbot diff --git a/.gitea/workflows/production-docker.yml b/.gitea/workflows/production-docker.yml new file mode 100644 index 0000000..c524d9b --- /dev/null +++ b/.gitea/workflows/production-docker.yml @@ -0,0 +1,45 @@ +name: NodBot Production Dockerization + +on: + pull_request: + branches: + - main + +env: + DHUB_UNAME: ${{ secrets.DHUB_UNAME }} + DHUB_PWORD: ${{ secrets.DHUB_PWORD }} + +jobs: + build: + runs-on: self-hosted + steps: + - name: Pull latest from Git + run: | + echo "Branch: ${{ gitea.head_ref }}" + pwd + whoami + mkdir -p /var/lib/act_runner/ + cd /var/lib/act_runner/ + if [ ! -d "nodbot" ]; then + git clone https://git.vfsh.dev/voidf1sh/nodbot + cd nodbot + else + cd nodbot + git pull + fi + git checkout ${{ gitea.head_ref }} + - name: Build the Docker image + run: | + cd /var/lib/act_runner/nodbot + docker build . --file Dockerfile --tag v0idf1sh/nodbot + - name: Log into Docker Hub + run: docker login -u $DHUB_UNAME -p $DHUB_PWORD + - name: Push image to Docker Hub + run: | + cd /var/lib/act_runner/nodbot + docker push v0idf1sh/nodbot + - name: Restart the container + run: | + cd /srv/docker/nodbot + docker-compose down + docker-compose up -d \ No newline at end of file diff --git a/.gitignore b/.gitignore index bd8ecf6..64a3651 100644 --- a/.gitignore +++ b/.gitignore @@ -114,3 +114,4 @@ dist # TernJS port file .tern-port +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5b969d8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,28 @@ +## v3.4.x +#### v3.4.0 (#25) +* Added nested commands, enclose a command in brackets, braces, or parenthesis inside a longer message: `You really don't get it do you? [that's the joke.gif] You're so dense` would return the results for just `that's the joke.gif` + +## v3.3.x +#### v3.3.3 (#20) +* Fixed content-list slash commands `/gifs`, `/pastas`, `/joints`, `/requests` (#19) +* Fixed the creation of duplicate commands properly (#18) +* Added a ton of aliases for `.gif` (`.wav`, `.mp3`, `.mp4`, `.wmv`, etc.) +* Added alias lists in `/help` + +#### v3.3.2 (#17) +* Fixed the `/help` command to not crash the bot (#15) +* Filtered out duplicate commands from the `/help` list, temporary fix (#18) +* Removed instances of `MessageEmbed.addField` due to deprecation (#16) + +v3.3.1 - Polishing and bugfixing for new AvWx commands +v3.3.0 - Added `.metar`, `.atis`, and `.datis` AvWx commands + +## v3.0.x +v3.0.1 - Migrate TenorJS API Endpoint +v3.0.2 - Add medical advice commands +v3.0.3 - Fix broken `/requests` command +v3.0.4 - Add ability to use multiple aliases +v3.0.5 - Add ability to save strains +v3.0.6 - Move `.strain` to `/strain` and add Autocomplete +v3.0.7 - Add `.spongebob` replies +v3.0.8 - Add ability to open requests by pages \ No newline at end of file diff --git a/CustomModules/ButtonHandlers.js b/CustomModules/ButtonHandlers.js new file mode 100644 index 0000000..cf3d6d9 --- /dev/null +++ b/CustomModules/ButtonHandlers.js @@ -0,0 +1,151 @@ +const customEmbeds = require('../CustomModules/Embeds.js'); +const InteractionStorage = require('../CustomModules/InteractionStorage.js'); +const indexer = require('../CustomModules/Indexer.js'); +const fn = require('../functions.js'); +const requests = require('../slash-commands/requests.js'); + +module.exports = { + baseEvent(interaction) { + let iStorage; + if (interaction.client.iStorage.has(interaction.message.interaction.id)) { + iStorage = interaction.client.iStorage.get(interaction.message.interaction.id) + } else { + iStorage = new InteractionStorage(interaction.message.interaction.id, interaction); + iStorage.page = 0; + } + if (interaction.user.id !== iStorage.userId) return; + switch (interaction.component.customId) { + // Any of the gifsPage Buttons + case 'prevGifsPage': + module.exports.gifsPage(interaction); + break; + case 'nextGifsPage': + module.exports.gifsPage(interaction); + break; + case 'prevPastasPage': + module.exports.pastasPage(interaction); + break; + case 'nextPastasPage': + module.exports.pastasPage(interaction); + break; + case 'prevRequestsPage': + module.exports.requestsPage(interaction); + break; + case 'nextRequestsPage': + module.exports.requestsPage(interaction); + break; + case 'prevJointsPage': + module.exports.jointsPage(interaction); + break; + case 'nextJointsPage': + module.exports.jointsPage(interaction); + break; + default: + return; + } + }, + gifsPage(interaction) { + const iStorage = interaction.client.iStorage.get(interaction.message.interaction.id); + + switch (interaction.component.customId) { + case 'prevGifsPage': + if (iStorage.page > 0) { + iStorage.page = iStorage.page - 1; + } + break; + case 'nextGifsPage': + if (iStorage.page < interaction.client.gifs.size / 10) { + iStorage.page = iStorage.page + 1; + } + break; + default: + break; + } + const indexedGifs = indexer(interaction.client.gifs, iStorage.page); + indexedGifs.gifsString = new String(); + + for (const gif of indexedGifs.thisPage) { + indexedGifs.gifsString += `[${gif.name}.gif](${gif.url})\n`; + } + + interaction.update(fn.embeds.gifs({command: "/gifs", author: interaction.member.displayName}, indexedGifs)); + }, + pastasPage(interaction) { + const iStorage = interaction.client.iStorage.get(interaction.message.interaction.id); + + switch (interaction.component.customId) { + case 'prevPastasPage': + if (iStorage.page > 0) { + iStorage.page = iStorage.page - 1; + } + break; + case 'nextPastasPage': + if (iStorage.page < interaction.client.pastas.size / 10) { + iStorage.page = iStorage.page + 1; + } + break; + default: + break; + } + const indexedPastas = indexer(interaction.client.pastas, iStorage.page); + indexedPastas.pastasString = new String(); + + for (const pasta of indexedPastas.thisPage) { + indexedPastas.pastasString += `${pasta.name}.pasta\n`; + } + + interaction.update(fn.embeds.pastas({command: "/pastas", author: interaction.member.displayName}, indexedPastas)); + }, + requestsPage(interaction) { + const iStorage = interaction.client.iStorage.get(interaction.message.interaction.id); + + switch (interaction.component.customId) { + case 'prevRequestsPage': + if (iStorage.page > 0) { + iStorage.page = iStorage.page - 1; + } + break; + case 'nextRequestsPage': + if (iStorage.page < interaction.client.requests.size / 10) { + iStorage.page = iStorage.page + 1; + } + break; + default: + break; + } + const indexedRequests = indexer(interaction.client.requests, iStorage.page); + indexedRequests.requestsString = new String(); + + for (const request of indexedRequests.thisPage) { + indexedRequests.requestsString += `[${request.id}]: ${request.request} (submitted by ${request.author})\n`; + } + + interaction.update(fn.embeds.requests({command: "/requests", author: interaction.member.displayName}, indexedRequests)); + }, + jointsPage(interaction) { + const iStorage = interaction.client.iStorage.get(interaction.message.interaction.id); + + switch (interaction.component.customId) { + case 'prevJointsPage': + if (iStorage.page > 0) { + iStorage.page = iStorage.page - 1; + } + break; + case 'nextJointsPage': + if (iStorage.page < interaction.client.joints.size / 10) { + iStorage.page = iStorage.page + 1; + } + break; + default: + break; + } + const indexedJoints = indexer(interaction.client.joints, iStorage.page); + indexedJoints.jointsString = new String(); + + for (const joint of indexedJoints.thisPage) { + indexedJoints.jointsString += `${joint.content}\n`; + } + + interaction.update(fn.embeds.joints({command: "/joints", author: interaction.member.displayName}, indexedJoints)); + } +} \ No newline at end of file diff --git a/CustomModules/Embeds.js b/CustomModules/Embeds.js new file mode 100644 index 0000000..115e66a --- /dev/null +++ b/CustomModules/Embeds.js @@ -0,0 +1,139 @@ +const { MessageActionRow, MessageButton } = require('discord.js'); + +module.exports = { + gifSearchAR(state) { + // Setup the buttons + const previousButton = new MessageButton() + .setCustomId('prevGif') + .setLabel('⬅️') + .setStyle('SECONDARY'); + + const confirmButton = new MessageButton() + .setCustomId('confirmGif') + .setLabel('✅') + .setStyle('PRIMARY'); + + const nextButton = new MessageButton() + .setCustomId('nextGif') + .setLabel('➡️') + .setStyle('SECONDARY'); + + const cancelButton = new MessageButton() + .setCustomId('cancelGif') + .setLabel('❌') + .setStyle('DANGER'); + + switch (state) { + case 'first': + previousButton.setDisabled(true); + break; + case 'last': + nextButton.setDisabled(true); + break; + } + + // Put the buttons into an ActionRow + return new MessageActionRow() + .addComponents(previousButton, confirmButton, nextButton, cancelButton); + }, + gifsPageAR(state) { + // Setup the buttons + const previousButton = new MessageButton() + .setCustomId('prevGifsPage') + .setLabel('⬅️') + .setStyle('SECONDARY'); + + const nextButton = new MessageButton() + .setCustomId('nextGifsPage') + .setLabel('➡️') + .setStyle('SECONDARY'); + + switch (state) { + case 'first': + previousButton.setDisabled(true); + break; + case 'last': + nextButton.setDisabled(true); + break; + } + + // Put the buttons into an ActionRow + return new MessageActionRow() + .addComponents(previousButton, nextButton); + }, + requestsPageAR(state) { + // Setup the buttons + const previousButton = new MessageButton() + .setCustomId('prevRequestsPage') + .setLabel('⬅️') + .setStyle('SECONDARY'); + + const nextButton = new MessageButton() + .setCustomId('nextRequestsPage') + .setLabel('➡️') + .setStyle('SECONDARY'); + + switch (state) { + case 'first': + previousButton.setDisabled(true); + break; + case 'last': + nextButton.setDisabled(true); + break; + } + + // Put the buttons into an ActionRow + return new MessageActionRow() + .addComponents(previousButton, nextButton); + }, + pastasPageAR(state) { + // Setup the buttons + const previousButton = new MessageButton() + .setCustomId('prevPastasPage') + .setLabel('⬅️') + .setStyle('SECONDARY'); + + const nextButton = new MessageButton() + .setCustomId('nextPastasPage') + .setLabel('➡️') + .setStyle('SECONDARY'); + + switch (state) { + case 'first': + previousButton.setDisabled(true); + break; + case 'last': + nextButton.setDisabled(true); + break; + } + + // Put the buttons into an ActionRow + return new MessageActionRow() + .addComponents(previousButton, nextButton); + }, + jointsPageAR(state) { + // Setup the buttons + const previousButton = new MessageButton() + .setCustomId('prevJointsPage') + .setLabel('⬅️') + .setStyle('SECONDARY'); + + const nextButton = new MessageButton() + .setCustomId('nextJointsPage') + .setLabel('➡️') + .setStyle('SECONDARY'); + + switch (state) { + case 'first': + previousButton.setDisabled(true); + break; + case 'last': + nextButton.setDisabled(true); + break; + } + + // Put the buttons into an ActionRow + return new MessageActionRow() + .addComponents(previousButton, nextButton); + } +} \ No newline at end of file diff --git a/CustomModules/Indexer.js b/CustomModules/Indexer.js new file mode 100644 index 0000000..dacb3bb --- /dev/null +++ b/CustomModules/Indexer.js @@ -0,0 +1,32 @@ +module.exports = (collection, page) => { + const itemsPerPage = 10; + const index = page * itemsPerPage; + const totalPages = Math.ceil(collection.size / itemsPerPage); + let state = page === 0 ? 'first' : 'middle'; + + const thisPage = new Array(); + + // Map the Djs Collection to an Array + const collectionArray = collection.map((command) => command); + + for (let i = index; i < index + itemsPerPage; i++) { + if (collectionArray[i]) { + thisPage.push(collectionArray[i]); + } else { + state = 'last'; + break; + } + + if (i === collectionArray.size - 1) { + state = 'last'; + break; + } + } + + return { + state: state, + thisPage: thisPage, + totalPages: totalPages, + pagesString: `${page + 1}/${totalPages}` + }; +} \ No newline at end of file diff --git a/CustomModules/InteractionStorage.js b/CustomModules/InteractionStorage.js new file mode 100644 index 0000000..6103ecd --- /dev/null +++ b/CustomModules/InteractionStorage.js @@ -0,0 +1,17 @@ +module.exports = class InteractionStorage { + constructor(idString, interaction) { + this.idString = idString; + this.userId = interaction.user.id; + + // Store in the client + interaction.client.iStorage.set(idString, this); + + // Delete this from the interactionStorage after 5 minutes + setTimeout(() => { + console.log(`Deleting interactionStorage with id: ${idString}`); + interaction.client.iStorage.delete(idString); + }, 300000); + + return this; + } +} \ No newline at end of file diff --git a/CustomModules/NodBot.js b/CustomModules/NodBot.js new file mode 100644 index 0000000..9b993c3 --- /dev/null +++ b/CustomModules/NodBot.js @@ -0,0 +1,179 @@ +module.exports = { + CommandData: class { + constructor(message) { + // Get the location of the final period in the message + this.finalPeriod = message.content.lastIndexOf('.'); + this.isCommand = this.finalPeriod >= 0 ? true : false; // Check if there is a period somewhere in the message to flag as a possible command + this.isValid = false; + this.args = message.content.slice(0,this.finalPeriod).toLowerCase(); // Grab everything leading up to the final period + this.command = message.content.slice(this.finalPeriod + 1).toLowerCase(); // Grab everything after the final period + this.author = message.author.username; + this.message = message; + + return this; + } + + validate(dotCommands) { + if (this.args.startsWith('http')) return false; + if (this.args.startsWith('www')) return false; + + const indices = { + curlyBrace: { + start: -1, + end: -1 + }, + bracket: { + start: -1, + end: -1 + }, + parenthesis: { + start: -1, + end: -1 + } + } + + // Check for and extract the part of the message that's + // wrapped in any type of brackets or quotes eg. ([{``''""}]) + const curlyBraceStart = this.message.content.match(/[\{]/g); + const curlyBraceEnd = this.message.content.match(/[\}]/g); + if (curlyBraceStart && curlyBraceEnd) { + indices.curlyBrace.start = this.message.content.indexOf(curlyBraceStart[0]) + 1; + indices.curlyBrace.end = this.message.content.lastIndexOf(curlyBraceEnd[0]); + } + + const bracketStart = this.message.content.match(/[\[]/g); + const bracketEnd = this.message.content.match(/[\]]/g); + if (bracketStart && bracketEnd) { + indices.bracket.start = this.message.content.indexOf(bracketStart[0]) + 1; + indices.bracket.end = this.message.content.lastIndexOf(bracketEnd[0]); + } + + const parenthesisStart = this.message.content.match(/[\(]/g); + const parenthesisEnd = this.message.content.match(/[\)]/g); + if (parenthesisStart && parenthesisEnd) { + indices.parenthesis.start = this.message.content.indexOf(parenthesisStart[0]) + 1; + indices.parenthesis.end = this.message.content.lastIndexOf(parenthesisEnd[0]); + } + + let nestedText = new String(); + + if (indices.curlyBrace.start >= 0 && indices.curlyBrace.end > 0) { + nestedText = this.message.content.slice(indices.curlyBrace.start, indices.curlyBrace.end); + } + + if (indices.bracket.start >= 0 && indices.bracket.end > 0) { + nestedText = this.message.content.slice(indices.bracket.start, indices.bracket.end); + } + + if (indices.parenthesis.start >= 0 && indices.parenthesis.end > 0) { + nestedText = this.message.content.slice(indices.parenthesis.start, indices.parenthesis.end); + } + + console.log(nestedText); + + if (nestedText !== "") { + this.nestedCommand = { + finalPeriod: nestedText.lastIndexOf('.'), + isCommand: nestedText.lastIndexOf('.') >= 0 ? true : false, + args: nestedText.slice(0, nestedText.lastIndexOf('.')).toLowerCase(), + command: nestedText.slice(nestedText.lastIndexOf('.') + 1).toLowerCase() + } + + for (const [key, value] of dotCommands) { + if (key === this.nestedCommand.command) { + this.isValid = true; + this.args = this.nestedCommand.args; + this.command = key; + this.isCommand = this.nestedCommand.isCommand; + this.finalPeriod = this.nestedCommand.finalPeriod; + return this; + } else if (value.alias) { + if (typeof value.alias === 'string' && value.alias === this.nestedCommand.command) { + this.command = key + this.args = this.nestedCommand.args; + this.isValid = true; + this.isCommand = this.nestedCommand.isCommand; + this.finalPeriod = this.nestedCommand.finalPeriod; + return this; + } else if (typeof value.alias === 'object' && value.alias.includes(this.nestedCommand.command)) { + this.command = key + this.args = this.nestedCommand.args; + this.isValid = true; + this.isCommand = this.nestedCommand.isCommand; + this.finalPeriod = this.nestedCommand.finalPeriod; + return this; + } + } + } + } + + for (const [key, value] of dotCommands) { + if (key === this.command) { + this.isValid = true; + return this; + } else if (value.alias) { + if (typeof value.alias === 'string' && value.alias === this.command) { + this.command = key; + this.isValid = true; + return this; + } else if (typeof value.alias === 'object' && value.alias.includes(this.command)) { + this.command = key; + this.isValid = true; + return this; + } + } + } + return this; + } + }, + GifData: class { + constructor() { + this.id = 0; + this.name = ""; + this.url = ""; + } + + // Initial GifData configuration + // Can also be used to update the data piecemeal + setInfo(name, url, id) { + // Check for existing or incoming name + if ((this.name === "") && (typeof name !== 'string')) throw `Error: This Gif doesn't have existing name, and no name is going to be set.`; + // Check for existing content or incoming content + if ((this.url === "") && (typeof url !== 'string')) throw `Error: This Gif doesn't have existing url, and no url is going to be set.`; + + // Property is set if the variable is the right type, + // otherwise it keeps the existing property + this.id = typeof id === 'number' ? id : this.id; + this.name = typeof name === 'string' ? name : this.name; + this.url = typeof url === 'string' ? url : this.url; + + return this; // For chaining + } + }, + PastaData: class { + constructor() { + this.id = 0; + this.name = ""; + this.content = ""; + this.iconUrl = ""; + } + + // Initial PastaData configuration + // Can also be used to update the data piecemeal + setInfo(name, content, iconUrl, id) { + // Check for existing or incoming name + if ((this.name === "") && (typeof name !== 'string')) throw `Error: This Pasta doesn't have existing name, and no name is going to be set.`; + // Check for existing content or incoming content + if ((this.content === "") && (typeof content !== 'string')) throw `Error: This Pasta doesn't have existing content, and no content is going to be set.`; + + // Property is set if the variable is the right type, + // otherwise it keeps the existing property + this.id = typeof id === 'number' ? id : this.id; + this.name = typeof name === 'string' ? name : this.name; + this.content = typeof content === 'string' ? content : this.content; + this.iconUrl = typeof iconUrl === 'string' ? iconUrl : this.iconUrl; + + return this; // For chaining + } + } +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 4905784..c29db9b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,4 +5,5 @@ WORKDIR /usr/src/app COPY package.json ./ RUN npm install COPY . . -CMD ["/bin/sh", "-c", "node main.js 2> /logs/nodbot.error 1> /logs/nodbot.log"] \ No newline at end of file +# CMD ["/bin/sh", "-c", "node main.js 2> /logs/nodbot.error 1> /logs/nodbot.log"] +CMD ["/bin/sh", "-c", "node main.js 2> /logs/$(date +%Y-%m-%d_%H-%M-%S)-error.txt 1> /logs/$(date +%Y-%m-%d_%H-%M-%S)-status.txt"] \ No newline at end of file diff --git a/README.md b/README.md index 7d31f60..799bc87 100644 --- a/README.md +++ b/README.md @@ -11,15 +11,6 @@ Use the `/help` command to see the bot's help message. ## Push Docker Image `docker push name/nodbot` -# Immediate To-Do - -1. ~~Sanitize inputs for SQL queries.~~ Done. -2. ~~Move environment variables so they don't get included in the image.~~ -3. Implement error handling on all actions. -4. Ephemeral responses to some/most slash commands. -5. Comment the code! Document! -6. Check for and create database tables if necessary. Handle errors. - # Deploy NodBot Yourself 1. Create an application at the [Discord Developer Portal](https://discord.com/developers/applications) @@ -97,15 +88,4 @@ tenorAPIKey= ownerId= statusChannelId= clientId= -``` - -## Changes - -v3.0.1 - Migrate TenorJS API Endpoint -v3.0.2 - Add medical advice commands -v3.0.3 - Fix broken `/requests` command -v3.0.4 - Add ability to use multiple aliases -v3.0.5 - Add ability to save strains -v3.0.6 - Move `.strain` to `/strain` and add Autocomplete -v3.0.7 - Add `.spongebob` replies -v3.0.8 - Add ability to open requests by pages \ No newline at end of file +``` \ No newline at end of file diff --git a/Roadmap.md b/Roadmap.md deleted file mode 100644 index 80bce7b..0000000 --- a/Roadmap.md +++ /dev/null @@ -1,14 +0,0 @@ -# v3.1.0 - -* Name checking for saving content -* .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. - -# v3.?.? -= Joke generator for Hallihan \ No newline at end of file diff --git a/config.json b/config.json index b9087cf..488a934 100644 --- a/config.json +++ b/config.json @@ -1,4 +1,7 @@ { "guildId": "868542949737246730", - "validCommands": [] + "validCommands": [], + "roaches": [], + "icaoIds": [], + "datisICAOs": [] } \ No newline at end of file diff --git a/dot-commands/datis.js b/dot-commands/datis.js new file mode 100644 index 0000000..e6948f7 --- /dev/null +++ b/dot-commands/datis.js @@ -0,0 +1,28 @@ +const fn = require('../functions'); + +module.exports = { + name: 'datis', + description: 'Lookup dATIS for an airport', + usage: 'ICAO.datis', + alias: [ 'atis' ], + async execute(message, commandData) { + try { + const icaoId = commandData.args.toUpperCase(); + if (icaoId.length !== 4) throw new Error('Invalid ICAO ID. Provide only one ICAO code at a time like KBOS'); + if (fn.avWx.datis.validate(icaoId)) { + const datisData = await fn.avWx.datis.getData(icaoId); + const messagePayload = fn.avWx.datis.parseData(datisData); + message.reply(messagePayload); + } else { + message.reply("No D-ATIS available for the specified ICAO ID."); + } + } catch (e) { + try { + message.reply(`D-ATIS Error: ${e.message}`); + console.error(e); + } catch (e) { + console.error(e); + } + } + } +} \ No newline at end of file diff --git a/dot-commands/gif.js b/dot-commands/gif.js index 9f5d8bb..370bf83 100644 --- a/dot-commands/gif.js +++ b/dot-commands/gif.js @@ -11,11 +11,12 @@ const tenor = new Tenor(process.env.TENOR_API_KEY); module.exports = { name: 'gif', description: 'Send a GIF', + alias: ['jpg', 'png', 'gifv', 'webm', 'mp4', 'wav', 'wmv', 'webp', 'mp3', 'flac', 'ogg', 'avi', 'mov', 'mpg', 'mpeg', 'mkv', 'flv', 'bmp', 'tiff', 'tif', 'svg', 'ico'], usage: '.gif', async execute(message, commandData) { // if (message.deletable) message.delete(); const client = message.client; - if (!client.gifs.has(commandData.args)) { + if (!client.gifs.has(commandData.args.toLowerCase())) { if (process.env.isDev) console.log('https://tenor.googleapis.com/v2/search?' + `&q=${commandData.args}` + `&key=${process.env.tenorAPIKey}` + '&limit=1&contentfilter=off'); tenor.search(commandData.args, "1").then(res => { if (res[0] == undefined) { @@ -30,11 +31,11 @@ module.exports = { }).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; + const gifData = client.gifs.get(commandData.args.toLowerCase()); // message.reply(fn.embeds.gif(commandData)); // message.channel.send(`> ${commandData.author} - ${commandData.args}.gif`); // message.channel.send(commandData.embed_url); - message.reply(commandData.embed_url); + message.reply(gifData.url); } } } \ No newline at end of file diff --git a/dot-commands/joint.js b/dot-commands/joint.js index 733dade..9abc9d1 100644 --- a/dot-commands/joint.js +++ b/dot-commands/joint.js @@ -5,12 +5,33 @@ module.exports = { name: 'joint', description: 'Send a random weed-themed phrase.', usage: '.joint', + alias: ['bong', 'blunt', 'bowl', 'pipe', 'dab', 'vape', 'dabs', 'shatter', 'edible', 'edibles', 'doobie', 'spliff', 'gummy', 'gummies', 'hash', 'toke', 'big doinks'], execute(message, commandData) { let joints = []; + // Create a simple array of the joint texts for (const entry of message.client.joints.map(joint => joint.content)) { joints.push(entry); } - const randIndex = Math.floor(Math.random() * joints.length); + // Generate a random number between 0 and the length of the joints array + let randIndex = Math.floor(Math.random() * joints.length); + // Grab the joint text from the array + let joint = joints[randIndex]; + + // Check if the joint has already been smoked + while (message.client.roaches.has(joint)) { + // Regenerate a random number and recheck + randIndex = Math.floor(Math.random() * joints.length); + joint = joints[randIndex]; + } + // Send the joint message.reply(`${joints[randIndex]} ${emoji.joint}`); + // Check how full the roach collection is + if (message.client.roaches.size / joints.length >= 0.85) { + // If the roach collection is 85% of the joints collection + // Empty it out + message.client.roaches.clear(); + } + // Add the joint to the roach collection + message.client.roaches.set(joint, "baked"); } } \ No newline at end of file diff --git a/dot-commands/metar.js b/dot-commands/metar.js new file mode 100644 index 0000000..33207e9 --- /dev/null +++ b/dot-commands/metar.js @@ -0,0 +1,26 @@ +const fn = require('../functions'); + +module.exports = { + name: 'metar', + description: 'Lookup METAR for an airport', + usage: 'ICAO.metar', + async execute(message, commandData) { + try { + // Parse the ICAOs into a CSV list by trimming whitespace and converting delimiters + // Also checks for validity of ICAOs + const icaoList = fn.avWx.parseICAOs(commandData); + const metarData = await fn.avWx.metar.getData(icaoList); + const messages = fn.avWx.metar.parseData(metarData); + messages.forEach(messagePayload => { + message.reply(messagePayload); + }); + } catch (e) { + try { + message.reply(`METAR Error: ${e.message}`); + console.error(e); + } catch (e) { + console.error(e); + } + } + } +} \ No newline at end of file diff --git a/dot-commands/pasta.js b/dot-commands/pasta.js index 8d391e8..cbf55ab 100644 --- a/dot-commands/pasta.js +++ b/dot-commands/pasta.js @@ -6,10 +6,11 @@ module.exports = { usage: '.pasta', execute(message, commandData) { const client = message.client; - let replyBody = ''; - let iconUrl; + let pastaData; if (!client.pastas.has(commandData.args)) { - commandData.content = 'Sorry I couldn\'t find that pasta.'; + pastaData = { + content: "Sorry, I couldn't find that pasta." + }; } else { commandData.content = client.pastas.get(commandData.args).content; } diff --git a/dot-commands/spongebob.js b/dot-commands/spongebob.js index f886a27..38f5bc0 100644 --- a/dot-commands/spongebob.js +++ b/dot-commands/spongebob.js @@ -10,20 +10,33 @@ module.exports = { // message.reply(fn.spongebob(commandData)).then(() => { // message.delete(); // }); - if (message.reference != undefined) { - const repliedMessageId = message.reference.messageId; - message.channel.messages.fetch(repliedMessageId) - .then(repliedMessage => { - repliedMessage.reply(fn.spongebob({ args: repliedMessage.content })).then(() => { - if (message.deletable) message.delete(); + if (message.reference != undefined) { // message.reference is undefined if the message isn't a reply to another message + if (commandData.args !== "") { // If the replying message isn't just .sb + const repliedMessageId = message.reference.messageId; // grab the message Id of the replied-to msg + message.channel.messages.fetch(repliedMessageId) // Fetch the message because with our luck it isn't in the cache + .then(repliedMessage => { + repliedMessage.reply(fn.spongebob({ args: commandData.args })).then(() => { // Use the pre-command text of the replying message to sb-ify + if (message.deletable) message.delete(); // If the initiating message is deletable, delete it. + }); + }) + .catch(err => { + console.error(err); }); - }) - .catch(err => { - console.error(err); - }); - } else { + } else { // We're working with a basic ".sb" and can proceed as we did before... + const repliedMessageId = message.reference.messageId; // grab the message Id of the replied-to msg + message.channel.messages.fetch(repliedMessageId) // Fetch the message because with our luck it isn't in the cache + .then(repliedMessage => { + repliedMessage.reply(fn.spongebob({ args: repliedMessage.content })).then(() => { + if (message.deletable) message.delete(); + }); + }) + .catch(err => { + console.error(err); + }); + } + } else { // The message isn't a reply, so just sb it like we did from the very beginning message.channel.send(fn.spongebob(commandData)).then(() => { - if (message.deletable) message.delete(); + if (message.deletable) message.delete(); // If the initiating message is deletable, delete it. }); } } diff --git a/functions.js b/functions.js index f812b7b..fad81b8 100644 --- a/functions.js +++ b/functions.js @@ -1,7 +1,6 @@ /* eslint-disable comma-dangle */ // dotenv for handling environment variables -const dotenv = require('dotenv'); -dotenv.config(); +const dotenv = require('dotenv').config(); // Assignment of environment variables for database access const dbHost = process.env.DB_HOST; const dbUser = process.env.DB_USER; @@ -14,6 +13,7 @@ const ownerId = process.env.ownerId; // filesystem const fs = require('fs'); +const zlib = require('zlib'); // Discord.js const Discord = require('discord.js'); @@ -21,14 +21,19 @@ const Discord = require('discord.js'); // Fuzzy text matching for db lookups const FuzzySearch = require('fuzzy-search'); +// Axios for APIs +const axios = require('axios'); + // Various imports from other files 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')); +const customEmbeds = require('./CustomModules/Embeds.js'); // MySQL database connection const mysql = require('mysql'); +const { GifData, PastaData } = require('./CustomModules/NodBot'); const db = new mysql.createPool({ connectionLimit: 10, host: dbHost, @@ -41,6 +46,11 @@ const db = new mysql.createPool({ const functions = { // Functions for managing and creating Collections collections: { + interactionStorage(client) { + if (!client.iStorage) client.iStorage = new Discord.Collection(); + client.iStorage.clear(); + if (isDev) console.log('Interaction Storage Collection Built'); + }, // Create the collection of slash commands slashCommands(client) { if (!client.slashCommands) client.slashCommands = new Discord.Collection(); @@ -72,13 +82,13 @@ const functions = { for (const file of dotCommandFiles) { const dotCommand = require(`./dot-commands/${file}`); client.dotCommands.set(dotCommand.name, dotCommand); - if (Array.isArray(dotCommand.alias)) { - dotCommand.alias.forEach(element => { - client.dotCommands.set(element, dotCommand); - }); - } else if (dotCommand.alias != undefined) { - client.dotCommands.set(dotCommand.alias, dotCommand); - } + // if (Array.isArray(dotCommand.alias)) { + // dotCommand.alias.forEach(element => { + // client.dotCommands.set(element, dotCommand); + // }); + // } else if (dotCommand.alias != undefined) { + // client.dotCommands.set(dotCommand.alias, dotCommand); + // } } if (isDev) console.log('Dot Commands Collection Built'); }, @@ -86,12 +96,13 @@ const functions = { 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); + // const gif = { + // id: row.id, + // name: row.name, + // embed_url: row.embed_url + // }; + const gifData = new GifData().setInfo(row.name, row.embed_url); + client.gifs.set(gifData.name, gifData); } if (isDev) console.log('GIFs Collection Built'); }, @@ -111,13 +122,8 @@ const functions = { 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); + const pastaData = new PastaData().setInfo(row.name, row.content, row.iconurl, row.id); + client.pastas.set(pastaData.name, pastaData); } if (isDev) console.log('Pastas Collection Built'); }, @@ -148,7 +154,7 @@ const functions = { if (isDev) console.log('Strains Collection Built'); }, medicalAdvice(rows, client) { - if (!client.medicalAdviceCol) client.medicalAdviceColl = new Discord.Collection(); + if (!client.medicalAdviceColl) client.medicalAdviceColl = new Discord.Collection(); client.medicalAdviceColl.clear(); for (const row of rows) { const medicalAdvice = { @@ -159,39 +165,10 @@ const functions = { } if (isDev) console.log('Medical Advice Collection Built'); }, - }, - dot: { - getCommandData(message) { - const commandData = {}; - // Split the message content at the final instance of a period - const finalPeriod = message.content.lastIndexOf('.'); - if(isDev) console.log(message.content); - // If the final period is the last character, or doesn't exist - if (finalPeriod < 0) { - if (isDev) console.log(finalPeriod); - commandData.isCommand = false; - return commandData; - } - commandData.isCommand = true; - // Get the first part of the message, everything leading up to the final period - commandData.args = message.content.slice(0,finalPeriod).toLowerCase(); - // Get the last part of the message, everything after the final period - 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); - // Add exceptions for messages that contain only a link - if (commandData.args.startsWith('http')) commandData.isValid = false; - } - else { - commandData.isValid = false; - console.error('Somehow a non-command made it to checkCommands()'); - } - return commandData; + roaches(client) { + if (!client.roaches) client.roaches = new Discord.Collection(); + client.roaches.clear(); + if (isDev) console.log('Medical Advice Collection Built'); } }, embeds: { @@ -199,52 +176,74 @@ const functions = { // Construct the Help Embed const helpEmbed = new Discord.MessageEmbed() .setColor('BLUE') - .setAuthor('Help Page') - .setDescription(strings.help.description) + .setAuthor({name: 'Help Page'}) .setThumbnail(strings.urls.avatar); // Construct the Slash Commands help let slashCommandsFields = []; - + let slashSeenNames = new Array(); const slashCommandsMap = interaction.client.slashCommands.map(e => { - return { - name: e.data.name, - description: e.data.description - }; - }) - + if (!slashSeenNames.includes(e.data.name)) { + slashSeenNames.push(e.data.name); + const command = { + name: e.data.name, + description: e.data.description + }; + return command; + } else { + return null; + } + }); for (const e of slashCommandsMap) { - slashCommandsFields.push({ - name: `- /${e.name}`, - value: e.description, - inline: false, - }); + slashCommandsFields.push(`- \`/${e.name}\` - ${e.description}`); } + console.log(slashCommandsFields); // Construct the Dot Commands Help - let dotCommandsFields = []; - + let dotCommandsFields = new Array(); + let dotSeenNames = new Array(); const dotCommandsMap = interaction.client.dotCommands.map(e => { - return { - name: e.name, - description: e.description, - usage: e.usage - }; + if (!dotSeenNames.includes(e.name)) { + dotSeenNames.push(e.name); + let command = { + name: e.name, + description: e.description, + usage: e.usage + }; + command.aliasString = new String(); + if (e.alias != undefined && typeof e.alias === 'object') { + for (const a of e.alias) { + command.aliasString += `\`.${a}\`, `; + } + } else if (e.alias != undefined && typeof e.alias === 'string') { + command.aliasString += `\`.${e.alias}\``; + } else { + command.aliasString = 'None'; + } + return command; + } else { + return null; + } }); - for (const e of dotCommandsMap) { - dotCommandsFields.push({ - name: `- .${e.name}`, - value: `${e.description}\nUsage: ${e.usage}`, - inline: false, - }); + if (e != null) { + dotCommandsFields.push(`- \`.${e.name}\` - ${e.description}\n\tUsage: ${e.usage}\n\tAliases: ${e.aliasString}`); + } } + console.log(dotCommandsFields); - helpEmbed.addField('Slash Commands', strings.help.slash); - helpEmbed.addFields(slashCommandsFields); - helpEmbed.addField('Dot Commands', strings.help.dot); - helpEmbed.addFields(dotCommandsFields); + // Construct the Description Fields + const descriptionFields = [ + `${strings.help.description}\n`, + `**Slash Commands**\n${strings.help.slash}\n`, + `${slashCommandsFields.join('\n')}\n`, + `**Dot Commands**\n${strings.help.dot}\n`, + `${dotCommandsFields.join('\n')}` + ]; + + // Set the description + helpEmbed.setDescription(descriptionFields.join('\n')); return { embeds: [ helpEmbed @@ -256,63 +255,49 @@ const functions = { pasta(commandData) { return commandData.content; }, - pastas(commandData) { - const pastasArray = []; + pastas(commandData, indexedPastas) { const pastasEmbed = new Discord.MessageEmbed() .setAuthor({name: commandData.command}) .setTimestamp() .setFooter({text: 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], ephemeral: true }; + const pastasPageAR = customEmbeds.pastasPageAR(indexedPastas.state); + return { embeds: [pastasEmbed], components: [pastasPageAR], ephemeral: true }; }, - gifs(commandData) { - const gifsArray = []; + gifs(commandData, indexedGifs) { const gifsEmbed = new Discord.MessageEmbed() - .setAuthor(commandData.command) + .setAuthor({name: commandData.command}) .setTimestamp() .setFooter({text: commandData.author}); - for (const row of commandData.gifs) { - gifsArray.push(`#${row.id} - ${row.name}.gif`); - } + const gifsPageAR = customEmbeds.gifsPageAR(indexedGifs.state); + return { embeds: [gifsEmbed], components: [gifsPageAR], ephemeral: true }; + }, + joints(commandData, indexedJoints) { + const jointsEmbed = new Discord.MessageEmbed() + .setAuthor({name: commandData.command}) + .setTimestamp() + .setFooter({text: `Page: ${indexedJoints.pagesString}`}) + .setDescription(indexedJoints.jointsString); - const gifsString = gifsArray.join('\n'); - gifsEmbed.setDescription(gifsString); - - return { embeds: [gifsEmbed] }; + const jointsPageAR = customEmbeds.jointsPageAR(indexedJoints.state); + return { embeds: [jointsEmbed], components: [jointsPageAR], ephemeral: true }; }, text(commandData) { return { embeds: [new Discord.MessageEmbed() - .setAuthor(commandData.command) + .setAuthor({name: commandData.command}) .setDescription(commandData.content) .setTimestamp() .setFooter({text: commandData.author})]}; }, - requests(commandData) { + requests(commandData, indexedRequests) { const requestsEmbed = new Discord.MessageEmbed() - .setAuthor(commandData.command) + .setAuthor({name: commandData.command}) .setTimestamp() .setFooter({text: 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], ephemeral: true }; + const requestsPageAR = customEmbeds.requestsPageAR(indexedRequests.state); + return { embeds: [requestsEmbed], components: [requestsPageAR], ephemeral: true }; }, strain(commandData, message) { const strainEmbed = new Discord.MessageEmbed() @@ -354,6 +339,88 @@ const functions = { message.reply({ embeds: [ strainEmbed ]}); }, + dalle(prompt, imageUrl, size) { + const dalleEmbed = new Discord.MessageEmbed() + .setAuthor({ name: "NodBot powered by DALL-E", iconURL: "https://assets.vfsh.dev/openai-logos/PNGs/openai-logomark.png" }) + .addFields( + { name: "Prompt", value: prompt } + ) + .setImage(imageUrl) + .setFooter({ text: `This ${size} image cost ${strings.costs.dalle[size]}¢ to generate.` }) + return { embeds: [dalleEmbed] }; + }, + gpt(prompt, response, usage) { + const gptEmbed = new Discord.MessageEmbed() + .setAuthor({ name: "NodBot powered by GPT-3.5", iconURL: "https://assets.vfsh.dev/openai-logos/PNGs/openai-logomark.png" }) + .setDescription(`**Prompt**\n${prompt}\n\n**Response**\n${response}`) + .setFooter({ text: `This prompt used ${usage.tokens} tokens for a cost of ${usage.usdc}¢. Generated using ${strings.ai.chatModel}` }) + return { embeds: [gptEmbed] }; + }, + generatingResponse() { + const embed = new Discord.MessageEmbed() + .setAuthor({ name: "NodBot powered by OpenAI", iconURL: "https://assets.vfsh.dev/openai-logos/PNGs/openai-logomark.png" }) + .setImage("https://media.tenor.com/aHMHzNGCb4kAAAAC/sucks.gif") + .setDescription("Generating a response, please stand by.") + .setFooter({ text: "Ligma balls" }); + return { embeds: [embed] }; + }, + avWx: { + metar(metarData) { + const wgst = metarData.wgst ? `G${metarData.wgst}` : ''; + const clouds = []; + const interAltim = Math.round((metarData.altim * 0.2952998057228486) * 10) + const altim = interAltim / 100; + metarData.clouds.forEach(cloudLayer => { + if (cloudLayer.base !== null) { + clouds.push(`${cloudLayer.cover} @ ${cloudLayer.base}'`); + } else { + clouds.push(`${cloudLayer.cover}`); + } + }); + const embed = new Discord.MessageEmbed() + .setAuthor({ name: `${metarData.name} [${metarData.icaoId}] METAR`, iconURL: "https://aviationweather.gov/img/icons/awc-logo-180.png"}) + // .setImage("https://media.discordapp.net/stickers/1175134632845516821.webp") + .setDescription(`**Do not use for real world flight planning or navigation.**`) + .setFooter({ text: "METAR by AviationWeather.gov for CumbHub LLC" }) + .addFields( + { name: 'Observation Time', value: `${metarData.reportTime}Z`, inline: true }, + { name: 'Temperature', value: `${metarData.temp}ºC/${metarData.dewp}ºC`, inline: true }, + { name: 'Winds', value: `${metarData.wdir.toString().padStart(3, '0')}º@${metarData.wspd}${wgst} kts`, inline: true }, + { name: 'Visibility', value: `${metarData.visib} SM`, inline: true }, + { name: 'Clouds', value: clouds.join('\n'), inline: true }, + { name: 'Altimeter', value: `${altim} inHg`, inline: true } + ) + return { content: metarData.rawOb, embeds: [embed] }; + }, + datis(datisData) { + const messageEmbed = new Discord.MessageEmbed() + .setAuthor({ name: `${datisData[0].airport} Digital ATIS` }) + // .setImage('https://media.discordapp.net/stickers/1175134632845516821.webp') + .setDescription(`**Do not use for real world flight planning or navigation.**`) + .setFooter({ text: 'D-ATIS by Clowd.io for CumbHub LLC' }) + + if (datisData.length > 1) { + datisData.forEach(data => { + if (data.type === 'dep') messageEmbed.addFields({ name: 'Departure Digital ATIS', value: data.datis, inline: false }); + if (data.type === 'arr') messageEmbed.addFields({ name: 'Arrival Digital ATIS', value: data.datis, inline: false }); + messageEmbed.addFields({ name: 'Information', value: data.code, inline: true }); + }) + messageEmbed.addFields( + { name: 'Retreival Time', value: `${new Date().toISOString()}`, inline: true } + ); + } else { + messageEmbed.addFields( + { name: 'Digital ATIS', value: datisData[0].datis, inline: false }, + { name: 'Information', value: `${datisData[0].code}`, inline: true }, + { name: 'Retreival Time', value: `${new Date().toISOString()}`, inline: true } + ) + } + + + const messagePayload = { embeds: [ messageEmbed ] }; + return messagePayload; + } + } }, collect: { gifName(interaction) { @@ -369,12 +436,13 @@ const functions = { functions.download.requests(client); }); }, - pasta(pastaData, client) { - const query = `INSERT INTO pastas (name, content) VALUES (${db.escape(pastaData.name)},${db.escape(pastaData.content)})`; - db.query(query, (err, rows, fields) => { + async pasta(pastaData, client) { + const query = `INSERT INTO pastas (name, content) VALUES (${db.escape(pastaData.name)},${db.escape(pastaData.content)}) ON DUPLICATE KEY UPDATE content=${db.escape(pastaData.content)}`; + await db.query(query, (err, rows, fields) => { if (err) throw err; functions.download.pastas(client); }); + return; }, joint(content, client) { const query = `INSERT INTO joints (content) VALUES (${db.escape(content)})`; @@ -383,9 +451,9 @@ const functions = { functions.download.joints(client); }); }, - gif(gifData, client) { - const query = `INSERT INTO gifs (name, embed_url) VALUES (${db.escape(gifData.name)}, ${db.escape(gifData.embed_url)})`; - db.query(query, (err, rows, fields) => { + async gif(gifData, client) { + const query = `INSERT INTO gifs (name, embed_url) VALUES (${db.escape(gifData.name)}, ${db.escape(gifData.url)}) ON DUPLICATE KEY UPDATE embed_url=${db.escape(gifData.url)}`; + await db.query(query, (err, rows, fields) => { if (err) throw err; functions.download.gifs(client); }); @@ -424,41 +492,71 @@ const functions = { } else { return 'Sorry, you don\'t have permission to do that.'; } + }, + medicalAdvice(content, client) { + const query = `INSERT INTO medical_advice (content) VALUES (${db.escape(content)})`; + db.query(query, (err, rows, fields) => { + if (err) throw err; + functions.download.medicalAdvice(client); + }); + }, + strain(interaction) { + const strain = db.escape(interaction.options.getString('name')); + const type = db.escape(interaction.options.getString('type')); + const effects = db.escape(( interaction.options.getString('effects') || 'Unkown' )); + const description = db.escape(( interaction.options.getString('description') || 'Unknown' )); + const flavor = db.escape(( interaction.options.getString('flavor') || 'Unknown' )); + const rating = db.escape(( interaction.options.getString('rating') || '3' )); + const strainQuery = `INSERT INTO strains (strain, type, effects, description, flavor, rating) VALUES (${strain}, ${type}, ${effects}, ${description}, ${flavor}, ${rating}) ON DUPLICATE KEY UPDATE strain=${db.escape(strain)}, type=${db.escape(type)}, effects=${db.escape(effects)}, description=${db.escape(description)}, flavor=${db.escape(flavor)}, rating=${db.escape(rating)}`; + console.log(strainQuery); + return new Promise((resolve, reject) => { + db.query(strainQuery, (err, rows, fields) => { + if (err) reject(err); + functions.download.strains(interaction.client); + resolve(); + }); + }) + }, + openai(user, prompt, engine, tokens, usdc) { + const query = `INSERT INTO openai (user, prompt, engine, tokens, usdc) VALUES (${db.escape(user)}, ${db.escape(prompt)}, ${db.escape(engine)}, ${db.escape(tokens)}, ${db.escape(usdc)})`; + db.query(query, (err) => { + if (err) throw err; + }); } }, download: { - requests(client) { + async requests(client) { const query = 'SELECT * FROM requests WHERE status = \'Active\' ORDER BY id DESC'; - db.query(query, (err, rows, fields) => { + await db.query(query, (err, rows, fields) => { if (err) throw err; functions.collections.requests(rows, client); }); }, - pastas(client) { + async pastas(client) { const query = 'SELECT * FROM pastas ORDER BY id ASC'; - db.query(query, (err, rows, fields) => { + await db.query(query, (err, rows, fields) => { if (err) throw err; functions.collections.pastas(rows, client); }); }, - gifs(client) { + async gifs(client) { const query = 'SELECT * FROM gifs ORDER BY id ASC'; - db.query(query, (err, rows, fields) => { + await db.query(query, (err, rows, fields) => { if (err) throw err; functions.collections.gifs(rows, client); }); }, - joints(client) { + async joints(client) { const query = 'SELECT * FROM joints ORDER BY id ASC'; - db.query(query, (err, rows, fields) => { + await db.query(query, (err, rows, fields) => { if (err) throw err; functions.collections.joints(rows, client); }); }, - strain(commandData, message) { + async strain(commandData, message) { const { strainName } = commandData; const query = `SELECT id, strain, type, effects, description, flavor, rating FROM strains WHERE strain = ${db.escape(strainName)}`; - db.query(query, (err, rows, fields) => { + await db.query(query, (err, rows, fields) => { if (rows != undefined) { commandData.strainInfo = { id: `${rows[0].id}`, @@ -473,16 +571,16 @@ const functions = { } }); }, - strains(client) { + async strains(client) { const query = 'SELECT id, strain FROM strains'; - db.query(query, (err, rows, fields) => { + await db.query(query, (err, rows, fields) => { if (err) throw err; functions.collections.strains(rows, client); }); }, - medicalAdvice(client) { + async medicalAdvice(client) { const query = 'SELECT * FROM medical_advice ORDER BY id ASC'; - db.query(query, (err, rows, fields) => { + await db.query(query, (err, rows, fields) => { if (err) throw err; functions.collections.medicalAdvice(rows, client); }); @@ -505,6 +603,16 @@ const functions = { } } }, + search: { + gifs(query, client) { + const gifSearcher = new FuzzySearch(client.gifs.map(element => element.name)); + return gifSearcher.search(query).slice(0,25); + }, + pastas(query, client) { + const pastaSearcher = new FuzzySearch(client.pastas.map(element => element.name)); + return pastaSearcher.search(query).slice(0,25); + } + }, // Parent-Level functions (miscellaneuous) closeRequest(requestId, interaction) { if (interaction.user.id == ownerId) { @@ -524,6 +632,7 @@ const functions = { }, spongebob(commandData) { let newText = ''; + let lastIsUpper = 0; for (const letter of commandData.args) { if (letter == ' ') { newText += letter; @@ -531,21 +640,190 @@ const functions = { } if (letter == 'i' || letter == 'I') { newText += 'i'; + lastIsUpper = 0; continue; } if (letter == 'l' || letter == 'L') { newText += 'L'; + lastIsUpper = 1; continue; } - if (Math.random() > 0.5) { + if (lastIsUpper === 0) { newText += letter.toUpperCase(); + lastIsUpper = 1; } else { newText += letter.toLowerCase(); + lastIsUpper = 0; } } return newText + ' <:spongebob:1053398825965985822>'; }, + autoresponses: { // Specific responses for certain keywords in sent messages + checkForAll(messageContent) { + let responses = []; + if (this.bigDoinks(messageContent)) responses.push("bigDoinks"); + if (this.ligma(messageContent)) responses.push("ligma"); + if (this.ong(messageContent)) responses.push("ong"); + if (this.fuckYou(messageContent)) responses.push("fuckYou"); + return responses; + }, + bigDoinks(messageContent) { + let count = 0; + const { keywords } = strings.autoresponses.bigDoinks; + keywords.forEach(e => { + if (messageContent.includes(e)) count++; + }); + if (count === keywords.length) { + return true; + } + }, + ligma(messageContent) { + let count = 0; + const { keywords } = strings.autoresponses.ligma; + keywords.forEach(e => { + if (messageContent.includes(e)) count++; + }); + if (count > 0) { + return true; + } + }, + ong(messageContent) { + let count = 0; + const { keywords } = strings.autoresponses.ong; + keywords.forEach(e => { + if (messageContent.includes(e)) count++; + }); + if (count > 0) { + return true; + } + }, + fuckYou(messageContent) { + let count = 0; + const { keywords } = strings.autoresponses.fuckYou; + keywords.forEach(e => { + if (messageContent.includes(e)) count++; + }); + if (count === keywords.length) { + return true; + } + }, + send(message, responseType) { + const { responses } = strings.autoresponses[responseType]; + const randomIndex = Math.floor(Math.random() * responses.length); + const response = responses[randomIndex]; + try { + message.reply(response); + } catch(e) { + console.log(new Error(e)); + } + } + }, + avWx: { + parseICAOs(commandData) { + let input = commandData.args.toUpperCase(); + // Replace newlines and different delimiters with a comma + let standardizedInput = input.replace(/[\s,;]+/g, ','); + + // Split the string by commas + let icaoArray = standardizedInput.split(','); + + // Trim each element to remove extra whitespace + icaoArray = icaoArray.map(icao => icao.trim()).filter(icao => icao.length > 0); + + icaoArray.forEach(icao => { + if (!(config.icaoIds.includes(icao))) throw new Error(`Invalid ICAO ID Detected: ${icao}`); + }); + + // Join the array into a comma-separated string + return icaoArray.join(','); + }, + metar: { + async getAllICAOs() { + const reqUrl = `https://aviationweather.gov/data/cache/stations.cache.json.gz` + try { + // Step 1: Download the GZipped file + const response = await axios({ + url: reqUrl, + method: 'GET', + responseType: 'arraybuffer', // Ensure we get the raw binary data + headers: { + 'Accept-Encoding': 'gzip' // Ensure the server sends gzipped content + } + }); + + // Step 2: Decompress the GZipped content + const buffer = Buffer.from(response.data); + zlib.gunzip(buffer, (err, decompressedBuffer) => { + if (err) { + console.error('An error occurred during decompression:', err); + return; + } + + // Step 3: Parse the decompressed JSON + const jsonString = decompressedBuffer.toString('utf-8'); + try { + const jsonData = JSON.parse(jsonString); + // console.log('Parsed JSON data:', jsonData); + jsonData.forEach(airport => { + config.icaoIds.push(airport.icaoId); + }); + // console.log(`ICAO IDs: ${config.icaoIds.length}\n\n${config.icaoIds}`) + } catch (jsonError) { + console.error('An error occurred while parsing JSON:', jsonError); + } + }); + } catch (error) { + console.error('An error occurred during the HTTP request:', error); + } + }, + async getData(icaoList) { + const reqUrl = `https://aviationweather.gov/api/data/metar?ids=${icaoList}&format=json`; + const response = await axios.get(reqUrl); + return response.data; + }, + parseData(metarData) { + let messages = []; + metarData.forEach(metar => { + messages.push(functions.embeds.avWx.metar(metar)); + }) + return messages; + } + }, + datis: { + async getAllICAOs() { + const reqUrl = 'https://datis.clowd.io/api/stations'; + const response = await axios.get(reqUrl); + response.data.forEach(icaoId => { + config.datisICAOs.push(icaoId); + }); + }, + validate(icaoId) { + return config.datisICAOs.includes(icaoId); + }, + async getData(icaoId) { + const reqUrl = `https://datis.clowd.io/api/${icaoId}`; + const response = await axios.get(reqUrl); + if (response.error !== undefined) throw new Error('The D-ATIS API returned an error:\n' + response.error); + return response.data; + }, + parseData(datisData) { + return functions.embeds.avWx.datis(datisData); + } + } + }, + generateErrorId() { + const digitCount = 10; + const digits = []; + for (let i = 0; i < digitCount; i++) { + const randBase = Math.random(); + const randNumRaw = randBase * 10; + const randNumRound = Math.floor(randNumRaw); + digits.push(randNumRound); + } + const errorId = digits.join(""); + return errorId; + } }; module.exports = functions; \ No newline at end of file diff --git a/main.js b/main.js index 7f15dab..1e49d7f 100644 --- a/main.js +++ b/main.js @@ -20,19 +20,25 @@ const { MessageActionRow, MessageButton } = require('discord.js'); const fn = require('./functions.js'); const config = require('./config.json'); const strings = require('./strings.json'); +const { GifData, CommandData } = require('./CustomModules/NodBot.js'); const isDev = process.env.IS_DEV; -client.once('ready', () => { +client.once('ready', async () => { + fn.collections.interactionStorage(client); 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); - fn.download.medicalAdvice(client); + fn.collections.roaches(client); + await fn.download.gifs(client); + await fn.download.pastas(client); + await fn.download.joints(client); + await fn.download.requests(client); + await fn.download.strains(client); + await fn.download.medicalAdvice(client); console.log('Ready!'); + await fn.avWx.metar.getAllICAOs(); + await fn.avWx.datis.getAllICAOs(); + // console.log(JSON.stringify(icaoArray)); client.channels.fetch(statusChannelId).then(channel => { channel.send(`${new Date().toISOString()} -- <@${process.env.OWNER_ID}>\nStartup Sequence Complete`); }); @@ -42,10 +48,14 @@ client.once('ready', () => { client.on('interactionCreate', async interaction => { if (interaction.isCommand()) { if (isDev) { - console.log(interaction); + console.log('Interaction ID: ' + interaction.id); } const { commandName } = interaction; + if (!client.iStorage.has(interaction.id)) { + new InteractionStorage(interaction.id, interaction); + } + if (client.slashCommands.has(commandName)) { client.slashCommands.get(commandName).execute(interaction); } else { @@ -55,7 +65,8 @@ client.on('interactionCreate', async interaction => { } if (interaction.isButton()) { - if (interaction.user.id != strings.temp.gifUserId) return; + if (isDev) console.log('Origin Interaction ID: ' + interaction.message.interaction.id); + if (isDev) console.log('Button ID: ' + interaction.component.customId); // Get some meta info from strings const index = strings.temp.gifIndex; const limit = strings.temp.gifLimit; @@ -101,10 +112,11 @@ client.on('interactionCreate', async interaction => { interaction.update(strings.temp.gifs[newIndex].embed_url); break; case 'confirmGif': - const gifData = { - name: strings.temp.gifName, - embed_url: strings.temp.gifs[strings.temp.gifIndex].embed_url, - }; + // const gifData = { + // name: strings.temp.gifName, + // url: strings.temp.gifs[strings.temp.gifIndex].embed_url, + // }; + const gifData = new GifData().setInfo(strings.temp.gifName, strings.temp.gifs[strings.temp.gifIndex].embed_url); fn.upload.gif(gifData, client); interaction.update({ content: `I've saved the GIF as ${gifData.name}.gif`, components: [] }); fn.download.gifs(interaction.client); @@ -163,20 +175,46 @@ client.on('interactionCreate', async interaction => { interaction.update({ content: 'Canceled.', components: [row] }); break; default: + ButtonHandlers.baseEvent(interaction); break; } } // Handle autocomplete requests if (interaction.isAutocomplete()) { - if (interaction.commandName == 'strain') { - const searchString = interaction.options.getFocused(); - const choices = fn.weed.strain.lookup(searchString, interaction.client); - await interaction.respond( - choices.map(choice => ({ name: choice, value: choice })) - ) - } else { - return; + switch (interaction.commandName) { + case 'strain': + const searchString = interaction.options.getFocused(); + const choices = fn.weed.strain.lookup(searchString, interaction.client); + await interaction.respond( + choices.map(choice => ({ name: choice, value: choice })) + ); + break; + case "edit": + //TODO + switch (interaction.options.getSubcommand()) { + case 'gif': + const gifQuery = interaction.options.getFocused(); + const gifChoices = fn.search.gifs(gifQuery, interaction.client); + await interaction.respond( + gifChoices.map(choice => ({ name: choice, value: choice })) + ); + break; + case 'pasta': + const pastaQuery = interaction.options.getFocused(); + const pastaChoices = fn.search.pastas(pastaQuery, interaction.client); + await interaction.respond( + pastaChoices.map(choice => ({ name: choice, value: choice })) + ); + break; + + default: + break; + } + break; + + default: + break; } } }); @@ -187,17 +225,15 @@ client.on('messageCreate', message => { if (message.author.bot) return; if (message.author.id == client.user.id) return; - // Wildcard Responses, will respond if any message contains the trigger word(s), excluding self-messages + // Automatic Responses, will respond if any message contains the keyword(s), excluding self-messages const lowerContent = message.content.toLowerCase(); - if (lowerContent.includes('big') && lowerContent.includes('doinks')) message.reply('gang.'); - if (lowerContent.includes('ligma')) message.reply('ligma balls, goteem'); - if (lowerContent.includes('frfr') || lowerContent.includes('fr fr') || lowerContent.includes('bussin') || lowerContent.includes(' ong') || lowerContent.startsWith('ong')) { - const randIndex = Math.floor(Math.random() * strings.capbacks.length); - message.reply(strings.capbacks[randIndex]); - } + const autoresponses = fn.autoresponses.checkForAll(lowerContent); + autoresponses.forEach(e => { + fn.autoresponses.send(message, e); + }); // Break the message down into its components and analyze it - const commandData = fn.dot.getCommandData(message); + const commandData = new CommandData(message).validate(message.client.dotCommands); console.log(commandData); if (commandData.isValid && commandData.isCommand) { diff --git a/package.json b/package.json index a7950d0..a454179 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "nodbot", - "version": "3.1.2", - "description": "Nods and Nod Accessories.", + "version": "3.4.0", + "description": "Nods and Nod Accessories", "main": "main.js", "dependencies": { "@discordjs/builders": "^1.9.0", diff --git a/slash-commands/edit.js b/slash-commands/edit.js new file mode 100644 index 0000000..975e933 --- /dev/null +++ b/slash-commands/edit.js @@ -0,0 +1,110 @@ +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 { GifData, PastaData } = require('../CustomModules/NodBot'); +const fn = require('../functions.js'); +const strings = require('../strings.json'); +const { emoji } = strings; + +module.exports = { + data: new SlashCommandBuilder() + .setName('edit') + .setDescription('Edit content in Nodbot\'s database.') +// GIF + .addSubcommand(subcommand => + subcommand + .setName('gif') + .setDescription('Edit a GIF URL') + .addStringOption(option => + option + .setName('name') + .setDescription('The name of the GIF to edit') + .setRequired(true) + .setAutocomplete(true) + ) + .addStringOption(option => + option + .setName('url') + .setDescription('The new URL') + .setRequired(true) + ) + ) +// Pasta + .addSubcommand(subcommand => + subcommand + .setName('pasta') + .setDescription('Edit a copypasta\'s content') + .addStringOption(option => + option + .setName('name') + .setDescription('The name of the copypasta') + .setRequired(true) + .setAutocomplete(true) + ) + .addStringOption(option => + option + .setName('content') + .setDescription('The new content of the copypasta') + .setRequired(true) + ) + ), + async execute(interaction) { + await interaction.deferReply({ ephemeral: true }); + try { + // Code Here... + const subcommand = interaction.options.getSubcommand(); + switch (subcommand) { +// GIF + case "gif": + //TODO + await this.editGif(interaction, interaction.options.getString('name'), interaction.options.getString('url')); + break; +// Joint + case "joint": + //TODO + break; +// MD + case "md": + //TODO + break; +// Pasta + case "pasta": + //TODO + await this.editPasta(interaction, interaction.options.getString('name'), interaction.options.getString('content')); + break; +// Strain + case "strain": + //TODO + break; + break; +// Default + default: + + break; + } + } catch (err) { + const errorId = fn.generateErrorId(); + console.error(`${errorId}: err`); + await interaction.editReply(`Sorry, an error has occured. Error ID: ${errorId}`); + } + }, + async editGif(interaction, name, url) { + const gifData = new GifData().setInfo(name, url); + await fn.upload.gif(gifData, interaction.client); + await fn.download.gifs(interaction.client); + await interaction.editReply(`I've updated ${gifData.name}.gif`); + }, + async editPasta(interaction, name, content) { + const pastaData = new PastaData().setInfo(name, content); + await fn.upload.pasta(pastaData, interaction.client); + await fn.download.gifs(interaction.client); + await interaction.editReply(`I've updated ${name}.pasta`); + } +}; \ No newline at end of file diff --git a/slash-commands/gifs.js b/slash-commands/gifs.js index 934ff86..c372cc0 100644 --- a/slash-commands/gifs.js +++ b/slash-commands/gifs.js @@ -1,33 +1,30 @@ const { SlashCommandBuilder } = require('@discordjs/builders'); const { config } = require('dotenv'); const fn = require('../functions.js'); +const indexer = require('../CustomModules/Indexer.js'); module.exports = { data: new SlashCommandBuilder() .setName('gifs') .setDescription('Get a list of currently saved GIFs.'), - async execute(interaction) { + 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, - }); + let iStorage = interaction.client.iStorage.get(interaction.id); + let indexedGifs = indexer(interaction.client.gifs, 0); + indexedGifs.gifsString = new String(); + + iStorage.page = 0; + + for (const gif of indexedGifs.thisPage) { + indexedGifs.gifsString += `[${gif.name}.gif](${gif.url})\n`; } - interaction.reply(fn.embeds.gifs(commandData)); - }, + const commandData = { + command: "/gifs", + author: interaction.member.displayName + }; + interaction.reply(fn.embeds.gifs(commandData, indexedGifs)); + } }; \ No newline at end of file diff --git a/slash-commands/help.js b/slash-commands/help.js index d18d9fe..084f2d1 100644 --- a/slash-commands/help.js +++ b/slash-commands/help.js @@ -4,29 +4,8 @@ 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')) - , + .setDescription('Send the help page.'), 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({content: 'I\'ve sent you a copy of my help page.', ephemeral: true}); - // }); - // break; - // default: - // interaction.reply('There was an error, please try again.'); - // break; - // } await interaction.reply(fn.embeds.help(interaction)); - }, + } }; \ No newline at end of file diff --git a/slash-commands/joints.js b/slash-commands/joints.js index 2ca5b6c..bca4490 100644 --- a/slash-commands/joints.js +++ b/slash-commands/joints.js @@ -1,15 +1,29 @@ const { SlashCommandBuilder } = require('@discordjs/builders'); const fn = require('../functions.js'); +const indexer = require('../CustomModules/Indexer.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({ content: 'Here are all the `.joint` phrases I have saved:\n\n' + joints.join('\n'), ephemeral: true }); + if (!interaction.client.joints) { + interaction.reply('For some reason I don\'t have access to the collection of joints. Sorry about that!'); + return; + } + let iStorage = interaction.client.iStorage.get(interaction.id); + let indexedJoints = indexer(interaction.client.joints, 0); + indexedJoints.jointsString = new String(); + + iStorage.page = 0; + + for (const joint of indexedJoints.thisPage) { + indexedJoints.jointsString += `${joint.content}\n`; + } + const commandData = { + command: "/joints", + author: interaction.member.displayName + }; + interaction.reply(fn.embeds.joints(commandData, indexedJoints)); }, }; \ No newline at end of file diff --git a/slash-commands/pastas.js b/slash-commands/pastas.js index baeb52d..8d43578 100644 --- a/slash-commands/pastas.js +++ b/slash-commands/pastas.js @@ -1,6 +1,7 @@ const { SlashCommandBuilder } = require('@discordjs/builders'); const { config } = require('dotenv'); const fn = require('../functions.js'); +const indexer = require('../CustomModules/Indexer.js'); module.exports = { data: new SlashCommandBuilder() @@ -11,23 +12,19 @@ module.exports = { interaction.reply({ content: 'For some reason I don\'t have access to the collection of copypastas. Sorry about that!', ephemeral: true }); 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, - }); + let iStorage = interaction.client.iStorage.get(interaction.id); + let indexedPastas = indexer(interaction.client.pastas, 0); + indexedPastas.pastasString = new String(); + + iStorage.page = 0; + + for (const pasta of indexedPastas.thisPage) { + indexedPastas.pastasString += `${pasta.name}.pasta\n`; } - interaction.reply(fn.embeds.pastas(commandData)); + const commandData = { + command: "/pastas", + author: interaction.member.displayName + }; + interaction.reply(fn.embeds.pastas(commandData, indexedPastas)); }, }; \ No newline at end of file diff --git a/slash-commands/requests.js b/slash-commands/requests.js index 039504e..fbc8b3b 100644 --- a/slash-commands/requests.js +++ b/slash-commands/requests.js @@ -1,39 +1,31 @@ const { SlashCommandBuilder } = require('@discordjs/builders'); const { config } = require('dotenv'); const fn = require('../functions.js'); +const indexer = require('../CustomModules/Indexer.js'); module.exports = { data: new SlashCommandBuilder() .setName('requests') - .setDescription('Get a list of Active requests from the database') - .addStringOption(option => - option - .setName('page') - .setDescription('Page Number') - .setRequired(true)), + .setDescription('Get a list of Active requests from the database'), async execute(interaction) { - const pageNum = interaction.options.getString('page'); - 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 (let i = ( 10 * ( pageNum - 1 ) ); i < ( 10 * pageNum ); i++) { - if (requestsMap[i] != undefined) { - commandData.requests.push({ - id: requestsMap[i].id, - author: requestsMap[i].author, - request: requestsMap[i].request, - }); - } + if (!interaction.client.requests) { + interaction.reply('For some reason I don\'t have access to the collection of requests. Sorry about that!'); + return; } - interaction.reply(fn.embeds.requests(commandData)); + let iStorage = interaction.client.iStorage.get(interaction.id); + let indexedRequests = indexer(interaction.client.requests, 0); + indexedRequests.requestsString = new String(); + + iStorage.page = 0; + + for (const request of indexedRequests.thisPage) { + indexedRequests.requestsString += `[${request.id}]: ${request.request} (submitted by ${request.author})\n`; + } + + const commandData = { + command: "/requests", + author: interaction.member.displayName + }; + interaction.reply(fn.embeds.requests(commandData, indexedRequests)); }, }; \ No newline at end of file diff --git a/slash-commands/save.js b/slash-commands/save.js new file mode 100644 index 0000000..28f7533 --- /dev/null +++ b/slash-commands/save.js @@ -0,0 +1,232 @@ +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'); +const { GifData } = require('../CustomModules/NodBot.js'); +const customEmbeds = require('../CustomModules/Embeds.js'); +const { emoji } = strings; + +module.exports = { + data: new SlashCommandBuilder() + .setName('save') + .setDescription('Save content to Nodbot\'s database.') +// GIF Search + .addSubcommand(subcommand => + subcommand + .setName('gifsearch') + .setDescription('Search Tenor for a GIF.') + .addStringOption(option => + option + .setName('query') + .setDescription('Search Query') + .setRequired(true)) + .addStringOption(option => + option + .setName('name') + .setDescription('What to save the gif as') + .setRequired(true)) + ) +// GIF URL + .addSubcommand(subcommand => + subcommand + .setName('gifurl') + .setDescription('Specify a URL to save.') + .addStringOption(option => + option + .setName('url') + .setDescription('URL Link to the GIF') + .setRequired(true)) + .addStringOption(option => + option + .setName('name') + .setDescription('What to save the gif as') + .setRequired(true)) + ) +// Joint + .addSubcommand(subcommand => + subcommand + .setName('joint') + .setDescription('Roll a joint and stash it in the database.') + .addStringOption(option => + option + .setName('joint-content') + .setDescription('Phrase to save') + .setRequired(true) + ) + ) +// MD + .addSubcommand(subcommand => + subcommand + .setName('md') + .setDescription('Add medical advice to the database.') + .addStringOption(option => + option + .setName('advice-content') + .setDescription('Advice to save') + .setRequired(true) + ) + ) +// Pasta + .addSubcommand(subcommand => + subcommand + .setName('pasta') + .setDescription('Save a copypasta to the database.') + .addStringOption(option => + option + .setName('pasta-name') + .setDescription('Title of the copypasta') + .setRequired(true) + ) + .addStringOption(option => + option + .setName('pasta-content') + .setDescription('Content of the copypasta') + .setRequired(true) + ) + ) +// Strain + .addSubcommand(subcommand => + subcommand + .setName('strain') + .setDescription('Store a new Strain in the database!') + .addStringOption(option => + option + .setName('name') + .setDescription('Name of the Strain') + .setRequired(true)) + .addStringOption(option => + option + .setName('type') + .setDescription('Indica/Sativa/Hybrid') + .setRequired(true) + .addChoices( + { name: "Indica", value: "Indica" }, + { name: "Hybrid", value: "Hybrid" }, + { name: "Sativa", value: "Sativa" } + ) + ) + .addStringOption(option => + option + .setName('effects') + .setDescription('The effects given by the strain') + .setRequired(false)) + .addStringOption(option => + option + .setName('flavor') + .setDescription('Flavor notes') + .setRequired(false)) + .addStringOption(option => + option + .setName('rating') + .setDescription('Number of stars') + .setRequired(false)) + .addStringOption(option => + option + .setName('description') + .setDescription('Description of the strain') + .setRequired(false)) + ), + async execute(interaction) { + await interaction.deferReply({ ephemeral: true }); + try { + // Code Here... + const subcommand = interaction.options.getSubcommand(); + switch (subcommand) { +// GIF Search + case "gifsearch": + // TODO Check on option names + const actionRow = customEmbeds.gifSearchAR(); + + // Get the query + const query = interaction.options.getString('query'); + strings.temp.gifName = interaction.options.getString('name').toLowerCase(); + + // 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.editReply('Sorry I was unable to find a GIF of ' + query); + return; + } + for (const row of res) { + strings.temp.gifs.push({ + embed_url: row.media_formats.gif.url, + }); + } + interaction.editReply({ content: strings.temp.gifs[0].embed_url, components: [actionRow], ephemeral: true }); + }); + break; +// GIF URL + case "gifurl": + //TODO Check on option names + // const gifData = { + // name: interaction.options.getString('name').toLowerCase(), + // url: interaction.options.getString('url'), + // }; + const gifData = new GifData().setInfo(interaction.options.getString('name').toLowerCase(), interaction.options.getString('url')); + fn.upload.gif(gifData, interaction.client); + interaction.editReply({ content: `I've saved the GIF as ${gifData.name}.gif`, ephemeral: true }); + fn.download.gifs(interaction.client); + break; +// Joint + case "joint": + //TODO + fn.upload.joint(interaction.options.getString('joint-content'), interaction.client); + interaction.editReply({ content: `The joint has been rolled${emoji.joint}`, ephemeral: true }); + break; +// MD + case "md": + //TODO + fn.upload.medicalAdvice(interaction.options.getString('advice-content'), interaction.client); + interaction.editReply({ content: `The advice has been saved!`, ephemeral: true }); + break; +// Pasta + case "pasta": + //TODO + const pastaData = { + name: interaction.options.getString('pasta-name').toLowerCase(), + content: interaction.options.getString('pasta-content'), + }; + await fn.upload.pasta(pastaData, interaction.client); + interaction.editReply({content: `The copypasta has been saved as ${pastaData.name}.pasta`, ephemeral: true }); + break; +// Strain + case "strain": + //TODO + fn.upload.strain(interaction).then(res => { + interaction.editReply({ + content: `The strain information has been saved. (${interaction.options.getString('name')})`, + ephemeral: true + }); + }).catch(err => { + console.log(`E: ${err}`); + interaction.editReply({ + content: 'There was a problem saving the strain.', + ephemeral: true + }); + }); + break; +// Default + default: + + break; + } + } catch (err) { + const errorId = fn.generateErrorId(); + console.error(`${errorId}: err`); + await interaction.editReply(`Sorry, an error has occured. Error ID: ${errorId}`); + } + } +}; \ No newline at end of file diff --git a/slash-commands/savegif.js b/slash-commands/savegif.js deleted file mode 100644 index 6971c3a..0000000 --- a/slash-commands/savegif.js +++ /dev/null @@ -1,93 +0,0 @@ -const tenor = require('tenorjs').client({ - 'Key': process.env.TENOR_API_KEY, // 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('Save a GIF to Nodbot\'s database.') - .addSubcommand(subcommand => - subcommand - .setName('searchgif') - .setDescription('Search Tenor for a GIF.') - .addStringOption(option => - option.setName('query') - .setDescription('Search Query') - .setRequired(true)) - .addStringOption(option => - option.setName('name') - .setDescription('What to save the gif as') - .setRequired(true)) - ) - .addSubcommand(subcommand => - subcommand - .setName('enterurl') - .setDescription('Specify a URL to save.') - .addStringOption(option => - option - .setName('url') - .setDescription('URL Link to the GIF') - .setRequired(true)) - .addStringOption(option => - option.setName('name') - .setDescription('What to save the gif as') - .setRequired(true)) - ), - async execute(interaction) { - if (interaction.options.getSubcommand() == 'searchgif') { - // 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'); - strings.temp.gifName = interaction.options.getString('name').toLowerCase(); - - // 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], ephemeral: true }); - }); - } - - if (interaction.options.getSubcommand() == 'enterurl') { - const gifData = { - name: interaction.options.getString('name'), - embed_url: interaction.options.getString('url'), - }; - fn.upload.gif(gifData, interaction.client); - interaction.reply({ content: `I've saved the GIF as ${gifData.name}.gif`, ephemeral: true }); - fn.download.gifs(interaction.client); - } - - }, -}; \ No newline at end of file diff --git a/slash-commands/savejoint.js b/slash-commands/savejoint.js deleted file mode 100644 index ba48eac..0000000 --- a/slash-commands/savejoint.js +++ /dev/null @@ -1,17 +0,0 @@ -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({ content: `The joint has been rolled${emoji.joint}`, ephemeral: true }); - }, -}; \ No newline at end of file diff --git a/slash-commands/savemd.js b/slash-commands/savemd.js deleted file mode 100644 index 8391da3..0000000 --- a/slash-commands/savemd.js +++ /dev/null @@ -1,17 +0,0 @@ -const { SlashCommandBuilder } = require('@discordjs/builders'); -const fn = require('../functions.js'); -// const { emoji } = require('../strings.json'); - -module.exports = { - data: new SlashCommandBuilder() - .setName('savemd') - .setDescription('Add medical advice to NodBot\'s Database!') - .addStringOption(option => - option.setName('advice-content') - .setDescription('What is the advice?') - .setRequired(true)), - async execute(interaction) { - fn.upload.medicalAdvice(interaction.options.getString('advice-content'), interaction.client); - interaction.reply({ content: `The advice has been saved!`, ephemeral: true }); - }, -}; \ No newline at end of file diff --git a/slash-commands/savepasta.js b/slash-commands/savepasta.js deleted file mode 100644 index a89da4a..0000000 --- a/slash-commands/savepasta.js +++ /dev/null @@ -1,24 +0,0 @@ -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({content: `The copypasta has been saved as ${pastaData.name}.pasta`, ephemeral: true }); - }, -}; \ No newline at end of file diff --git a/slash-commands/savestrain.js b/slash-commands/savestrain.js deleted file mode 100644 index 3cc90f8..0000000 --- a/slash-commands/savestrain.js +++ /dev/null @@ -1,54 +0,0 @@ -const { SlashCommandBuilder } = require('@discordjs/builders'); -const fn = require('../functions.js'); -const { emoji } = require('../strings.json'); - -// Strain Name | Type | Effects | Flavor | Rating | Description - -module.exports = { - data: new SlashCommandBuilder() - .setName('savestrain') - .setDescription('Store a new Strain in the database!') - .addStringOption(option => - option.setName('name') - .setDescription('Name of the Strain') - .setRequired(true)) - .addStringOption(option => - option.setName('type') - .setDescription('Indica/Sativa/Hybrid') - .setRequired(true) - .addChoices( - { name: "Indica", value: "Indica" }, - { name: "Hybrid", value: "Hybrid" }, - { name: "Sativa", value: "Sativa" } - )) - .addStringOption(option => - option.setName('effects') - .setDescription('The effects given by the strain') - .setRequired(false)) - .addStringOption(option => - option.setName('flavor') - .setDescription('Flavor notes') - .setRequired(false)) - .addStringOption(option => - option.setName('rating') - .setDescription('Number of stars') - .setRequired(false)) - .addStringOption(option => - option.setName('description') - .setDescription('Description of the strain') - .setRequired(false)), - async execute(interaction) { - fn.upload.strain(interaction).then(res => { - interaction.reply({ - content: `The strain information has been saved. (${interaction.options.getString('name')})`, - ephemeral: true - }); - }).catch(err => { - console.log(`E: ${err}`); - interaction.reply({ - content: 'There was a problem saving the strain.', - ephemeral: true - }); - }); - }, -}; \ No newline at end of file diff --git a/strings.json b/strings.json index bcf8a14..8883464 100644 --- a/strings.json +++ b/strings.json @@ -1,8 +1,8 @@ { "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'" + "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>", @@ -14,13 +14,71 @@ "urls": { "avatar": "https://cdn.discordapp.com/avatars/513184762073055252/12227aa23a06d5178853e59b72c7487b.webp?size=128" }, - "capbacks": [ - "on god?!", - "fr fr?!", - "no cap?!", - "no cap fr", - "bussin fr, no cap", - "ongggg no :billed_cap: fr fr" - ], + "costs": { + "gpt": { + "gpt-3.5-turbo": 0.2 + }, + "dalle": { + "256x256": 1.6, + "512x512": 1.8, + "1024x1024": 2.0 + } + }, + "ai": { + "chatModel": "gpt-3.5-turbo", + "chatPromptCentsPer": 0.15, + "chatPromptUnits": 1000, + "chatResCentsPer": 0.2, + "chatResUnits": 1000 + }, + "autoresponses": { + "bigDoinks": { + "keywords": ["big", "doinks"], + "responses": [ + "<:bigdoinks:1053706618853924905> Gang.", + "<:bigdoinks:1053706618853924905> Out here in Amish", + "<:bigdoinks:1053706618853924905> Out here in Amish, smoking Big Doinks in Amish... Gang." + ] + }, + "ligma": { + "keywords": ["ligma"], + "responses": [ + "ligma balls lmao gottem", + "ligma balls ahaha", + "https://tenor.com/view/ligma-balls-gif-12236083", + "<:deadmonkey:1139186312444911707>" + ] + }, + "ong": { + "keywords": [ + "frfr", + "fr fr", + "bussin", + "no cap", + " ong " + ], + "responses": [ + "on god?!", + "fr fr?!", + "no cap?!", + "no cap fr", + "bussin fr, no cap", + "ongggg no :billed_cap: fr fr" + ] + }, + "fuckYou": { + "keywords": [ + "fuck", + "nodbot" + ], + "responses": [ + "no u", + "go fuck yourself", + "why does everyone hate me :sob:", + "<:kms:1253790048696926298>", + "Eat a bag of dicks" + ] + } + }, "temp": {} } \ No newline at end of file diff --git a/update.sh b/update.sh new file mode 100644 index 0000000..6380ff3 --- /dev/null +++ b/update.sh @@ -0,0 +1,4 @@ +#!/bin/bash +git pull +docker build . -t v0idf1sh/nodbot +docker push v0idf1sh/nodbot \ No newline at end of file