v3 to Prod

This commit is contained in:
Skylar Grant 2021-09-22 13:15:31 -04:00
parent 5c8ea22626
commit c6045361d0
60 changed files with 1251 additions and 995 deletions

22
.github/workflows/deploy.yaml vendored Normal file
View File

@ -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

View File

@ -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.

View File

@ -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 `<phrase>.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.

29
TODO.md Normal file
View File

@ -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

39
_deploy-commands.js Normal file
View File

@ -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);
}
})();

View File

@ -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: '<IATA>',
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);
});
}
}

View File

@ -1,11 +0,0 @@
const fn = require('../functions.js');
module.exports = {
name: 'closereq',
description: 'Close a given request by ID',
usage: '<request_id>',
execute(message, file) {
fn.closeRequest(file.name);
message.channel.send(fn.textEmbed('Request closed.', message.author, file.extension));
}
}

View File

@ -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: '<GIF name or Search Query>',
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`));
}
}
}

View File

@ -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.'));
}
}

View File

@ -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));
},
};

View File

@ -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}`);
}
}

View File

@ -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'));
}
}

View File

@ -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));
}
}
}

View File

@ -1,20 +0,0 @@
const functions = require('../functions.js');
module.exports = {
name: 'pasta',
description: 'Send a copypasta.',
usage: '<Copypasta Name>',
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));
}
}

View File

@ -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.'));
}
}

View File

@ -1,7 +0,0 @@
module.exports = {
name: 'ping',
description: 'Pong!',
execute(message, args) {
message.channel.send('Pong!');
}
}

View File

@ -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');
}
}

View File

@ -1,13 +0,0 @@
const fn = require('../functions.js');
module.exports = {
name: 'request',
description: 'Submit a request to the bot developer.',
usage: '<request or feedback>',
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);
}
}

View File

@ -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);
}
}

View File

@ -1,11 +0,0 @@
const functions = require('../functions.js');
module.exports = {
name: 'roll',
description: 'Add a phrase to the .joint command',
usage: '<phrase to save>',
execute(message, file) {
functions.uploadPotPhrase(file.name);
message.channel.send('"' + file.name + '" has been added to the list');
}
}

View File

@ -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: '<search query>',
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));
})
}
}

View File

@ -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: '<Pasta Name>',
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));
}
}

View File

@ -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));
}
}

View File

@ -1,21 +0,0 @@
const functions = require('../functions.js');
module.exports = {
name: 'spongebob',
description: 'SpOnGeBoB-iFy AnYtHiNg AuToMaTiCaLly',
usage: '<text to convert>',
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}`);
}
}

View File

@ -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);
});
}
}

View File

@ -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);
});
}
}

View File

@ -1,9 +0,0 @@
const fn = require('../functions.js');
module.exports = {
name: '',
description: '',
execute(message, file) {
}
}

View File

@ -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");
}
}

View File

@ -1,11 +0,0 @@
module.exports = {
name: 'upload',
description: '',
execute(message, file) {
const fn = require('../functions');
fn.uploadGIFs(message);
message.reply('Uploaded, I hope.');
}
}

View File

@ -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: '<ZIP code, City Name, etc>',
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);
});
}
}

View File

@ -1,9 +0,0 @@
module.exports = {
name: "wrongbad",
description: "",
execute(message, args) {
const wrongbad = "<:wrongbad:853304921969393684>";
message.channel.send("");
}
}

View File

@ -1,3 +1,8 @@
{
"validExtensions": []
"isDev": true,
"clientId": "513184762073055252",
"guildId": "760701839427108874",
"devChannelId": "868545469381480498",
"guildName": "CumbHub",
"validCommands": []
}

36
dot-commands/gif.js Normal file
View File

@ -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 name or Search Query>.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);
}
}
}

19
dot-commands/pasta.js Normal file
View File

@ -0,0 +1,19 @@
const fn = require('../functions.js');
module.exports = {
name: 'pasta',
description: 'Send a copypasta.',
usage: '<Copypasta Name>.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));
}
}

17
dot-commands/request.js Normal file
View File

@ -0,0 +1,17 @@
const fn = require('../functions.js');
module.exports = {
name: 'request',
description: 'Submit a request to the bot developer.',
usage: '<request or feedback>.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);
},
};

34
dot-commands/spongebob.js Normal file
View File

@ -0,0 +1,34 @@
const functions = require('../functions.js');
const config = require('../config.json');
module.exports = {
name: 'spongebob',
description: 'SpOnGeBoB-iFy AnYtHiNg AuToMaTiCaLly',
usage: '<text to convert>.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}`);
}
}

17
dot-commands/strain.js Normal file
View File

@ -0,0 +1,17 @@
const fn = require('../functions');
module.exports = {
name: 'strain',
description: 'Search for information about a cannabis strain.',
usage: '<strain name>.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));
}
}
}

View File

@ -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);
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);
}
},
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);
}
if (config.isDev) console.log('Slash Commands Collection Built');
},
getGifFiles(client) {
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();
const query = "SELECT name, embed_url FROM gifs";
return new Promise((resolve, reject) => {
db.query(query)
.then(res => {
for (let row of res.rows) {
for (const row of rows) {
const gif = {
id: row.id,
name: row.name,
embed_url: row.embed_url
};
client.gifs.set(gif.name, gif);
}
resolve();
})
.catch(err => console.error(err));
});
if (config.isDev) console.log('GIFs Collection Built');
},
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 = {
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.potphrases.set(potphrase.id, potphrase);
client.joints.set(joint.id, joint);
}
})
.catch(err => console.error(err));
if (config.isDev) console.log('Joints Collection Built');
},
getPastaFiles(client) {
pastas(rows, 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) {
for (const row of rows) {
const pasta = {
id: row.id,
name: row.name,
content: row.content,
iconUrl: row.iconurl
iconUrl: row.iconurl,
};
client.pastas.set(pasta.name, pasta);
}
resolve();
})
.catch(err => console.error(err));
});
if (config.isDev) console.log('Pastas Collection Built');
},
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}\``
});
}
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 = [];
requests(rows, client) {
if (!client.requests) client.requests = new Discord.Collection();
client.requests.clear();
for (const row of rows) {
fields.push({
name: '#' + row.id,
value: row.request + `\nSubmitted by ${row.author}`
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,
});
}
return new Discord.MessageEmbed()
.setAuthor('NodBot Requests')
.setTitle('Currently Active Requests')
.addFields(fields)
.setTimestamp();
// 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
]};
},
closeRequest(id) {
const query = `UPDATE requests SET status = 'Closed' WHERE id = ${id}`;
db.query(query)
.then()
.catch(e => console.error(e));
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);
});
}
},
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);
});
},
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);
});
},
},
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;

View File

@ -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));
}
});

View File

@ -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
Dot-Extension Commands: [* means updated]
*.gif
*.pasta
*.request
*.spongebob
*.strain

204
main.js
View File

@ -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);

View File

@ -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": {

View File

@ -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.`);
},
};

33
slash-commands/gifs.js Normal file
View File

@ -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));
},
};

30
slash-commands/help.js Normal file
View File

@ -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;
}
},
};

17
slash-commands/joint.js Normal file
View File

@ -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}`);
},
};

15
slash-commands/joints.js Normal file
View File

@ -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'));
},
};

11
slash-commands/lenny.js Normal file
View File

@ -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('( ͡° ͜ʖ ͡°)');
},
};

33
slash-commands/pastas.js Normal file
View File

@ -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));
},
};

11
slash-commands/ping.js Normal file
View File

@ -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!');
},
};

20
slash-commands/reload.js Normal file
View File

@ -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!');
},
};

View File

@ -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));
},
};

56
slash-commands/savegif.js Normal file
View File

@ -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] });
});
},
};

View File

@ -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}`);
},
};

View File

@ -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`);
},
};

11
slash-commands/template Normal file
View File

@ -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
},
};

11
slash-commands/truth.js Normal file
View File

@ -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');
},
};

View File

@ -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": "❌"
}
}

18
strings.json Normal file
View File

@ -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": {}
}

23
testing.txt Normal file
View File

@ -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 ]