Javascript - discord.js - How to simply repeated options in SlashCommandBuilder - javascript

I found that my command ended up needing many repeated options which were all similar. There are 4 players and 5 towers each and with my method it would take 20 separate inputs to address them all. This would be monotonous to input as the user and I was wondering if there was a way to simplify the inputting process for the user or just the code. I haven't found any direct solutions from the discord.js library.
Also, there is a built in limit of 25 options and I have gone over this amount.
Here is my initial attempt. Note that all the inputs use the same autocomplete options. Scroll to where it says //these lines
//party command
const { SlashCommandBuilder } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('party')
.setDescription('Creates a party which others can join based on roles')
.addStringOption(option =>
option.setName('gamemode')
.setDescription('The name of the gamemode')
.setRequired(true)
.addChoices(
{ name: 'Hardcore', value: 'Hardcore' },
{ name: 'Fallen', value: 'Fallen' },
{ name: 'Molten', value: 'Molten' },
{ name: 'Normal', value: 'Normal' },
{ name: 'Badlands II', value: 'Badlands II' },
{ name: 'Polluted Wastelands II', value: 'Polluted Wastelands II' },
{ name: 'Pizza Party', value: 'Pizza Party' },
{ name: 'Other', value: 'Other' },
))
.addStringOption(option =>
option.setName('strategy')
.setDescription('The name of the strategy you will use - skip this option for no strategy ')
)
.addStringOption(option =>
option.setName('strategy-link')
.setDescription('The link to your strategy - skip this option for no strategy')
)
//these lines v
.addStringOption(option =>
option.setName('p1-1')
.setDescription('First tower for p1 - skip for no tower/player')
.setAutocomplete(true)
)
.addStringOption(option =>
option.setName('p1-2')
.setDescription('Second tower for p1 - skip for no tower/player')
.setAutocomplete(true)
),
// p1-3, p1-4, etc.
async autocomplete(interaction) {
const focusedOption = interaction.options.getFocused(true);
let choices;
if (focusedOption.name === 'p1-1', 'p1-2') {
//these lines ^
choices = [
'Scout',
'Sniper',
'Paintballer',
'Demoman',
'Soldier',
'Solder',
'Freezer',
'Hunter',
'Ace Pilot',
'Medic',
'Pyromancer',
'Farm',
'Militant',
'Shotgunner',
'Military Base',
'Rocketeer',
'Electroshocker',
'Commander',
'DJ Booth',
'Warden',
'Minigunner',
'Ranger',
'Accelerator',
'Engineer',
'Crook Boss',
'Turret',
'Mortar',
'Pursuit',
'Golden Minigunner',
'Golden Pyromancer',
'Golden Crook Boss',
'Golden Scout',
'Golden Cowboy',
'Golden Soldier',
'Frost Blaster',
'Swarmer',
'Toxic Gunner',
'Sledger',
'Executioner',
'Elf Camp',
'Archer',
];
}
const filtered = choices.filter(choice => choice.startsWith(focusedOption.value));
let options;
if (filtered.length > 25) {
options = filtered.slice(0, 25);
} else {
options = filtered;
}
await interaction.respond(
options.map(choice => ({ name: choice, value: choice })),
);
},
async execute(interaction) {
const gamemode = interaction.options.getString('gamemode');
const strategy = interaction.options.getString('strategy') ?? 'no';
await interaction.reply(`You created a party for ${gamemode} using ${strategy} strategy.`);
},
};
Node.js v18.12.1
npm list discord.js#12.5.3 dotenv#16.0.3
Thank you for your help.

Related

How do I specify options for commands?

I want to specify choices for an option for my command in Discord.js. How do I do that?
The command:
module.exports = {
name: 'gifsearch',
description: 'Returns a gif based on your search term.',
options: [{
name: 'type',
type: 'STRING',
description: 'Whether to search for gifs or stickers.',
choices: //***this is the area where I have a question***
required: true
},
{
name: 'search_term',
type: 'STRING',
description: 'The search term to use when searching for gifs.',
required: true,
}],
async execute(interaction) {
let searchTerm = interaction.options.getString('search_term')
const res = fetch(`https://api.giphy.com/v1/gifs/search?q=${searchTerm}&api_key=${process.env.giphyAPIkey}&limit=1&rating=g`)
.then((res) => res.json())
.then((json) => {
if (json.data.length <= 0) return interaction.reply({ content: `No gifs found!` })
interaction.reply({content: `${json.data[0].url}`})
})
},
};
I have read the discord.js documentation/guide and I know about the .addChoice() method, but it doesn't look like that will be compatible with my bot's current code.
The discord.js api describes this as ApplicationCommandOptionChoices.
So you basically just insert an array of this in your choices.
module.exports = {
...
choices: [
{
name: "name to display",
value: "the actual value"
},
{
name: "another option",
value: "the other value"
}
]
...
};

How to reduce an array objects based on an array of items

How do I turn the following array of objects (packageMap) into a string of names based on an array of matching id's
I am imagining it to be a combination of filter and map but I can't understand it!
const packageMap = [
{
id: `g3IbvxHPGt4xUVfYK`,
name: `Package 1 - Cost £10`
},
{
id: `HPGKCapDCJS76sgd7`,
name: `Package 2 - Cost £20`
},
{
id: `jbd73hndxJH6U6j6s`,
name: `Package 3 - Cost £1`
},
{
id: `5F53DSndxJH6nns22`,
name: `Package 3 - Cost £5`
}
];
const matchingIDs = ['5F53DSndxJH6nns22', 'HPGKCapDCJS76sgd7'];
If I can somehow get an array of names from packageMap to look like this
let packages = ['Package 3 - Cost £5', 'Package 2 - Cost £20']
I can then use join on packages
let joined = packages.join(' and ')
let outStr = `You are subscribed to ${joined}`
console.log(outStr)
// "You are subscribed to Package 3 - Cost £5 and Package 2 - Cost £20"
I am using node so not constrained by old browsers or anything like that, so latest versions of answers are fine.
packageMap.filter(({ id }) => matchingIDs.includes(id)).map(({ name }) => name)
This would work:
const packageMap = [
{
id: `g3IbvxHPGt4xUVfYK`,
name: `Package 1 - Cost £10`
},
{
id: `HPGKCapDCJS76sgd7`,
name: `Package 2 - Cost £20`
},
{
id: `jbd73hndxJH6U6j6s`,
name: `Package 3 - Cost £1`
},
{
id: `5F53DSndxJH6nns22`,
name: `Package 3 - Cost £5`
}
];
const matchingIDs = ['5F53DSndxJH6nns22', 'HPGKCapDCJS76sgd7'];
let packages = matchingIDs.map(x => packageMap.find(y => y.id == x).name)
console.log(packages)
Use reduce and includes
const packageMap = [
{
id: `g3IbvxHPGt4xUVfYK`,
name: `Package 1 - Cost £10`,
},
{
id: `HPGKCapDCJS76sgd7`,
name: `Package 2 - Cost £20`,
},
{
id: `jbd73hndxJH6U6j6s`,
name: `Package 3 - Cost £1`,
},
{
id: `5F53DSndxJH6nns22`,
name: `Package 3 - Cost £5`,
},
];
const matchingIDs = ["5F53DSndxJH6nns22", "HPGKCapDCJS76sgd7"];
const names = packageMap.reduce(
(acc, { id, name }) => (matchingIDs.includes(id) ? [...acc, name] : acc),
[]
);
console.log(names);

How to filter JavaScript Objects with multiple criterias

I've got a JSON file with recipes.
Now there is a search bar on my homepage where the user can check and uncheck checkboxes to select which attribute of the recipe (name, ingredients, tags, category) he wants to search in. He can also check multiple criteria.
Now I want to filter my object based on the selected criteria.
I know if there is for example only checked the "name" I can just go for
recipes.filter(e -> e.name.indexOf(searchString) >= 0)
But how can I say dynamically "If also the ingredients are checked filter for a found result in the name OR in the ingredients".
I hope you understand. Thank you.
You can put all the attributes in an array and then use .some() to check if any of the attributes matches.
const recipes = [
{ name: "pizza", ingredients: "dough tomato mince", category: "meal" },
{ name: "pie", ingredients: "dough sugar", category: "dessert" },
{ name: "stew", ingredients: "potato mince onoin", category: "meal" },
{ name: "donut", ingredients: "sugar", category: "dessert" }
];
// Get these from the checkboxes:
const selected_attributes = [
"name",
"ingredients"
];
// Get this from the searchbar:
const seachbar_query = "do";
// Filter all recipes where the name or ingredients contains "do".
// We expect "pizza" and "pie', which contain dough in their ingredients.
// And we also expect "donuts", whose name starts with "do"
const filtered = recipes.filter( recipe => {
return selected_attributes.some( attribute => {
return recipe[ attribute ].includes( seachbar_query );
});
});
console.log( filtered );
filter is a higher order function that takes a predicate function that either returns true or false depending on the current item should be kept or not. In your case the function is a single one-liner, but any kind of function could be passed.
So if you have complex logic, I suggest that you move it out into a named function, where you can check for several conditions, and finally return a single boolean value:
function isRecipeValid(recipe) {
if (recipe.something) return false;
// any arbitrary conditional checking here
return recipe.conditionOne && recipe.conditionTwo;
}
recipes.filter(isRecipeValid);
write a function.
recipes.filter(e => {
if (whatever) return true;
else {
// additional checks.
}
});
in your case, I guess something like this:
recipes.filter(e => {
if (ingredientsAreChecked()) {
return e.name.matchesSearchString() || e.ingredients.some(i => i.matchesSearchString());
}
else {
return e.name.matchesSearchString();
}
});
or if ingredientsAreChecked() is not a computationally light thing to do, then do something like this:
if (ingredientsAreChecked()) return recipes.filter(e => ...);
else return recipes.filter(e => ...);
So you have two things: search term and fields to search.
You can build a filtering function that takes those two, and a single record (single recipe) and returns true or false.
Say your checkboxes are name, description, ingreedients.
What you do is you send your filter function the item name, but also the name of the fields you wanna search. Then insert the values there.
You could have something like this:
// Disclaimer: these recipes are made up
const recipes = [{
name: 'lemon cake',
description: 'a delicious cake',
ingreedients: ['lemon', 'flour', 'sugar'],
},
{
name: 'sour lemon cake',
description: 'a delicious cake',
ingreedients: ['lemon', 'flour', 'not sugar'],
},
{
name: 'choco brownie',
description: 'a sweet chocolate desert',
ingreedients: ['chocolate', 'milk', 'flour', 'salt', 'sugar'],
},
{
name: 'vanilla croissant',
description: 'a yummy pastry with vanilla flavour',
ingreedients: ['vanilla', 'milk', 'flour'],
}
];
// To search, we need the search term, and an array of fields by which to search
// We return ANY match, meaning if the search term is in any of the fields, it's a match
function searchAnyField(searchTerm, searchFields) {
return recipes.filter(item => {
for (let field of searchFields) {
if (item[field].indexOf(searchTerm) > -1) {
return true;
}
}
return false;
});
}
// the same, but we make sure the search term exists in ALL fields (seems dumb here, but could be a req)
function searchAllFields(searchTerm, searchFields) {
return recipes.filter(item => {
// A naive approach is to count in how many fields did we find the term and if it matches the search fields length
let foundInFields = 0;
for (let field of searchFields) {
if (item[field].indexOf(searchTerm) > -1) {
foundInFields++;
}
}
return foundInFields === searchFields.length;
});
}
// Let's see it in action
// we'lll just print out the names of found items
console.log('Brownie in name:', printNames(
searchAnyField('brownie', ['name'])));
console.log('Cake in name:', printNames(
searchAnyField('cake', ['name'])));
// Let's try multiple fields
console.log('Choc in name and desc:', printNames(
searchAnyField('choc', ['name', 'description'])));
console.log('flour anywhere:', printNames(
searchAnyField('flour', ['name', 'description', 'ingreedients'])));
console.log('sweet anywhere:', printNames(
searchAnyField('sweet', ['name', 'description', 'ingreedients'])));
// How about AND search:
console.log('cake in name AND desc:', printNames(
searchAllFields('cake', ['name', 'description'])));
console.log('cake everywhere:', printNames(
searchAllFields('cake', ['name', 'description', 'ingreedients'])));
function printNames(recipes) {
return recipes.map(r => r.name).join(', ');
}
Edit: You also said you have some nested props and whatnot. Here are more examples of how you could go about it.
const FIELDS = {
name: {
type: 'string',
path: 'name',
},
description: {
type: 'string',
path: 'name',
},
ingreedients: {
type: 'array',
path: 'name',
},
price: {
type: 'nested',
path: 'details.price',
nestedType: 'number',
}
}
// Disclaimer: these recipes are made up
const recipes = [{
name: 'lemon cake',
description: 'a delicious cake',
ingreedients: ['lemon', 'flour', 'sugar'],
details: {
price: 45,
}
},
{
name: 'sour lemon cake',
description: 'a delicious cake',
ingreedients: ['lemon', 'flour', 'not sugar'],
details: {
price: 45,
}
},
{
name: 'choco brownie',
description: 'a sweet chocolate desert',
ingreedients: ['chocolate', 'milk', 'flour', 'salt', 'sugar'],
details: {
price: 42,
},
},
{
name: 'vanilla croissant',
description: 'a yummy pastry with vanilla flavour',
ingreedients: ['vanilla', 'milk', 'flour'],
details: {
price: 45,
},
}
];
// To search, we need the search term, and an array of fields by which to search
// We return ANY match, meaning if the search term is in any of the fields, it's a match
function searchAnyField(searchTerm, searchFields) {
return recipes.filter(item => {
for (let field of searchFields) {
switch (field.type) {
case 'string':
if (item[field.path].indexOf(searchTerm) > -1) {
return true;
}
case 'array':
if (item[field.path].includes(searchTerm) > -1) {
return true;
}
case 'nested':
const props = field.path.split('.').reverse();
let prop = props.pop();
let val = item[prop];
while (val && props.length > 0) {
prop = props.pop();
val = val[prop]
}
if (field.nestedType === 'string') {
if (val && val.indexOf(searchTerm) > -1) {
return true;
}
} else if (field.nestedType === 'number') {
return val == searchTerm;
}
}
}
});
return false;
}
// Let's see it in action
// we'lll just print out the names of found items
console.log('42 anywhere:', printNames(
searchAnyField(42, [ FIELDS.price])));
console.log('42 anywhere:', printNames(
searchAnyField(42, [ FIELDS.price, FIELDS.name])));
function printNames(recipes) {
return recipes.map(r => r.name).join(', ');
}

overwritePermissions not functioning| Discord.js MASTER

I am in the process of converting my bot to work with the master branch of discord.js
I got to my ticket command and they changed a lot, I managed to do everything apart from the overwritePermissions section. I am unsure of why the code is not working.
If I remove the bottom 2 overwritePermissions sections, it works fine, but with all 3 present, none execute.
let role = message.channel.guild.defaultRole;
let role2 = message.guild.roles.find(x => x.name === "Support Team");
message.guild.channels.create(`ticket-test`, {type: 'text'}).then(c => {
c.overwritePermissions({
permissionOverwrites: [{
id: role.id,
deny: ['SEND_MESSAGES', 'VIEW_CHANNEL'],
}]
});
c.overwritePermissions({
permissionOverwrites: [{
id: role2.id,
deny: ['SEND_MESSAGES', 'VIEW_CHANNEL'],
}]
});
c.overwritePermissions({
permissionOverwrites: [{
id: message.author.id,
allow: ['SEND_MESSAGES', 'VIEW_CHANNEL'],
}]
});
});
I have done console.log(role.id) and console.log(role2.id and they both show the correct id, it's just not executing the code.
Instead of repeating the overwritePermissions() method, you can simply list your permission overwrites in the channel creation options in the first place. type's default is already a text channel, so you can also omit that option. Finally, you should always be catching Promises.
const everyone = message.guild.defaultRole;
const support = message.guild.roles.find(r => r.name === 'Support Team');
message.guild.channels.create('ticket-test', {
permissionOverwrites: [
{
id: everyone.id,
deny: 'VIEW_CHANNEL'
}, {
id: support.id,
allow: 'VIEW_CHANNEL'
}, {
id: message.author.id,
allow: 'VIEW_CHANNEL'
}
]
}).catch(err => console.error(err));
let role2 = message.guild.roles.find(x => x.name === "💻Mods");
but if you want
for this:
async function jointocreatechannel(user) {
//log it
console.log(" :: " + user.member.user.username + "#" + user.member.user.discriminator + " :: Created a Support")
//user.member.user.send("This can be used to message the member that a new room was created")
await user.guild.channels.create(`${user.member.user.username}'s Support`, {
type: 'voice',
parent: "973217461577080903", //or set it as a category id
}).then(async vc => {
//move user to the new channel
user.setChannel(vc);
//set the new channel to the map
jointocreatemap.set(`tempvoicechannel_${vc.guild.id}_${vc.id}`, vc.id);
//change the permissions of the channel
let role2 = message.guild.roles.find(x => x.name === "💻Mods");
await vc.overwritePermissions([
{
id: user.id,
allow: ['MANAGE_CHANNELS'],
},
{
id: user.guild.id,
deny: ['VIEW_CHANNEL'],
},
{
id: role2.id,
deny: ['VIEW_CHANNEL'],
},
]);
})
}
}

discord.js-commando if else loop

I've made a purge function for my bot, but if someone doesn't input the right number of messages to purge I want it to rerun the command. Only problem is I can't seem to figure out what to do.
This is what I've got far:
const { Command } = require('discord.js-commando');
module.exports = class SayCommand extends Command {
constructor(client) {
super(client, {
name: 'purge',
group: 'group1',
memberName: 'purge',
description: 'Deletes messages from the current channel.',
examples: ['purge'],
args: [
{
key: 'count',
prompt: 'How many messages do you want to delete?',
type: 'integer'
}
]
});
}
run(msg, { count }) {
if(count < 1 || count > 30) {
msg.channel.bulkDelete(count).then(() => {
msg.say('Deleted ' + count + ' messages.').then(msg => msg.delete(3000));
});
} else {
msg.say('You can delete 1 to 30 messages at a time.');
}
}
};
After this it stops and you need to call the command again.
Is there a way of looping this until the user inputs a correct number?
Thanks in advance!
You could just let Commando do this by providing a min and max value:
args: [
{
key: 'count',
prompt: 'How many messages do you want to delete?',
type: 'integer',
min: 1,
max: 30,
error: 'You can delete 1 to 30 messages at a time.'
}
]
Docs

Categories

Resources