I got this jumbled words code where it ends if nobody got the right answer after the time has ran out. But the collector doesn't end somehow. The code works just fine but I wanted the collector to end in a given limited time.
This was the code:
const listword = ['array', 'of', 'words'];
var word = listword[Math.floor(Math.random() * listword.length)].toLowerCase();
var jumbled = word
.split('')
.sort(function () {
return 0.5 - Math.random();
})
.join('');
interaction.followUp({ content: `The word is ${word}`, ephemeral: true });
await interaction.channel.send(`Guess the word \`${jumbled}\`!`);
const filter = (response) => listword.includes(response.content.trim());
const collector = interaction.channel.createMessageCollector(filter, {
time: 15000, //this doesn't work somehow
});
collector.on('collect', (response) => {
if (response.author.bot) return;
var guessword = response.content.trim();
if (guessword.toLowerCase() === word) {
response.react('✅');
response.reply({
content: `${response.author} guessed it right! The word was \`${word}\`.`,
allowedMentions: { users: [], roles: [], repliedUser: false },
});
collector.stop();
} else {
if (response.author.bot) return;
response.react('❌');
}
});
collector.on('end', (collected) => {
if (!collected.size) {
interaction.channel.send(
'Time is up! Nobody was able to guess the word' + `\`${word}\`.`
);
}
});
I don't know why the collector doesn't end unless the word is guessed correctly. What do I need do to fix it?
In discord.js v13, all Collector related classes and methods (both .create*() and .await*()) takes a single object parameter. That means, createMessageCollector accepts a single parameter, an options object. (previously, the first parameter was a filter and the second one is the options).
As you provide the filter as the first argument, the object with the time key is simply ignored. Just pass a single object with the filter and the time and it will work fine:
const collector = interaction.channel.createMessageCollector({
filter,
time: 15000,
});
Similar answer: https://stackoverflow.com/a/70455975/6126373
Related
My Mongoose schema uses a custom _id value and the code I inherited does something like this
const sampleSchema = new mongoose.Schema({
_id: String,
key: String,
});
sampleSchema.statics.generateId = async function() {
let id;
do {
id = randomStringGenerator.generate({length: 8, charset: 'hex', capitalization: 'uppercase'});
} while (await this.exists({_id: id}));
return id;
};
let SampleModel = mongoose.model('Sample', sampleSchema);
A simple usage looks like this:
let mySample = new SampleModel({_id: await SampleModel.generateId(), key: 'a' });
await mySample.save();
There are at least three problems with this:
Every save will require at least two trips to the database, one to test for a unique id and one to save the document.
For this to work, it is necessary to manually call generateId() before each save. An ideal solution would handle that for me, like Mongoose does with ids of type ObjectId.
Most significantly, there is a potential race condition that will result in duplicate key error. Consider two clients running this code. Both coincidentally generate the same id at the same time, both look in the database and find the id absent, both try to write the record to the database. The second will fail.
An ideal solution would, on save, generate an id, save it to the database and on duplicate key error, generate a new id and retry. Do this in a loop until the document is stored successfully.
The trouble is, I don't know how to get Mongoose to let me do this.
Here's what I tried: Based on this SO Question, I found a rather old sample (using a very old mongoose version) of overriding the save function to accomplish something similar and based this attempt off it.
// First, change generateId() to force a collision
let ids = ['a', 'a', 'a', 'b'];
let index = 0;
let generateId = function() {
return ids[index++];
};
// Configure middleware to generate the id before a save
sampleSchema.pre('validate', function(next) {
if (this.isNew)
this._id = generateId();
next();
});
// Now override the save function
SampleModel.prototype.save_original = SampleModel.prototype.save;
SampleModel.prototype.save = function(options, callback) {
let self = this;
let retryOnDuplicate = function(err, savedDoc) {
if (err) {
if (err.code === 11000 && err.name === 'MongoError') {
self.save(options, retryOnDuplicate);
return;
}
}
if (callback) {
callback(err, savedDoc);
}
};
return self.save_original(options, retryOnDuplicate);
}
This gets me close but I'm leaking a promise and I'm not sure where.
let sampleA = new SampleModel({key: 'a'});
let sampleADoc = await sampleA.save();
console.log('sampleADoc', sampleADoc); // prints undefined, but should print the document
let sampleB = new SampleModel({key: 'b'});
let sampleBDoc = await sampleB.save();
console.log('sampleBDoc', sampleBDoc); // prints undefined, but should print the document
let all = await SampleModel.find();
console.log('all', all); // prints `[]`, but should be an array of two documents
Output
sampleADoc undefined
sampleBDoc undefined
all []
The documents eventually get written to the database, but not before the console.log calls are made.
Where am I leaking a promise? Is there an easier way to do this that addresses the three problems I outlined?
Edit 1:
Mongoose version: 5.11.15
I fixed the problem by changing the save override. The full solution looks like this:
const sampleSchema = new mongoose.Schema({
_id: String,
color: String,
});
let generateId = function() {
return randomStringGenerator.generate({length: 8, charset: 'hex', capitalization: 'uppercase'});
};
sampleSchema.pre('validate', function() {
if (this.isNew)
this._id = generateId();
});
let SampleModel = mongoose.model('Sample', sampleSchema);
SampleModel.prototype.save_original = SampleModel.prototype.save;
SampleModel.prototype.save = function(options, callback) {
let self = this;
let isDupKeyError = (error, field) => {
// Determine whether the error is a duplicate key error on the given field
return error?.code === 11000 && error?.name === 'MongoError' && error?.keyValue[field];
}
let saveWithRetries = (options, callback) => {
// save() returns undefined if used with callback or a Promise otherwise.
// https://mongoosejs.com/docs/api/document.html#document_Document-save
let promise = self.save_original(options, callback);
if (promise) {
return promise.catch((error) => {
if (isDupKeyError(error, '_id')) {
return saveWithRetries(options, callback);
}
throw error;
});
}
};
let retryCallback;
if (callback) {
retryCallback = (error, saved, rows) => {
if (isDupKeyError(error, '_id')) {
saveWithRetries(options, retryCallback);
} else {
callback(error, saved, rows);
}
}
}
return saveWithRetries(options, retryCallback);
}
This will generate an _id repeatedly until a successful save is called and addresses the three problems outlined in the original question:
The minimum trips to the database has been reduced from two to one. Of course, if there are collisions, more trips will occur but that's the exceptional case.
This implementation takes care of generating the id itself with no manual step to take before saving. This reduces complexity and removes the required knowledge of prerequisites for saving that are present in the original method.
The race condition has been addressed. It won't matter if two clients attempt to use the same key. One will succeed and the other will generate a new key and save again.
To improve this:
There ought to be a maximum number of save attempts for a single document followed by failure. In this case, you've perhaps used up all the available keys in whatever domain you're using.
The unique field may not be named _id or you might have multiple fields that require a unique generated value. The embedded helper function isDupKeyError() could be updated to look for multiple keys. Then on error you could add logic to regenerate just the failed key.
Im trying to make a queue system in discord.js for my bot. When I add 2 songs to test with the command it puts them in the queue like this:
[
'https://www.youtube.com/watch?v=Jgv7qhDLPhg&ab_channel=BruhSoundEffects',
'https://www.youtube.com/watch?v=Jgv7qhDLPhg&ab_channel=BruhSoundEffects'
]
The first songs plays out fine and works but then when it comes to the transitions the sounds just stops. The console returns no error, probably there's a logical error, but I can not figure out where it is going wrong.
module.exports = class Youtube {
constructor(){
this.playQueue = [];
this.dispatcher = null;
}
playYoutubeMusic(channel, message, args, volume = 0.2) {
if(!args[0]) return message.channel.send('no link provided');
if(!this.validYtLink(args[0])) return message.channel.send('Invalid yt link');
console.log(this.playQueue.length);
const _this = this;
if(this.playQueue.length == 0){
this.playQueue.push(args[0]);
console.log(this.playQueue.length);
channel.join().then(connection => {
this.play(false, connection);
this.dispatcher.setVolume(volume);
this.dispatcher.on("finish", function () {
console.log("test");
console.log(_this.playQueue);
if (_this.playQueue.length != 0) {
_this.play(true, connection);
}
else connection.disconnect();
});
});
} else {
this.playQueue.push(args[0]);
console.log(this.playQueue);
}
}
play(shift = false, connection){
console.log("test");
if(shift) this.playQueue = this.playQueue.shift();
this.dispatcher = connection.play(ytdl(this.playQueue[0], { quality: 'highestaudio' }));
}
channel is from const channel = client.channels.cache.get("626133529130172432");
message is from the client.on("message", function(message)
args in this case is the link
I just don't understand how the first song plays fine without any problems but when it comes to the second song the bot just stops but there is no error in the console.
Array.shift() in JavaScript mutates the array. It removes the first element from an array and returns that removed element.
When you assign the return value from the shift() in your play() method, you update this.playQueue and set it to the removed element.
At this moment, this.playQueue is no longer an array but a string, and when you call ytdl(this.playQueue[0], ...) the second time, you call the function with the first character of that string (h from https://...) instead of the link.
As shift() mutates the array, you don't need to reassign it:
play(shift = false, connection){
console.log("test");
if(shift) {
// shift() updates this.playQueue, no need to reassign
this.playQueue.shift();
}
this.dispatcher = connection.play(ytdl(this.playQueue[0], { quality: 'highestaudio' }));
}
Firstly, some background as to what my test script will cover.
Pressing a button on a website will fire off an email to a test mailbox.
This email can take anything between 10 and 30 minutes to arrive in the test mailbox.
So using the following code from imap-simple ;
'get new email info': function(browser) {
imaps.connect(config).then(function (connection) {
return connection.openBox('INBOX').then(function () {
var searchCriteria = ['UNSEEN'];
var fetchOptions = {
bodies: ['HEADER', 'TEXT'],
markSeen: false
};
return connection.search(searchCriteria, fetchOptions).then(function (results) {
var subjects = results.map(function (res) {
return res.parts.filter(function (part) {
return part.which === 'HEADER';
})[0].body.subject[0];
});
console.log(subjects);
Correctly comes back with a blank subjects array, as the email hasn't been received by the test mailbox yet.
Adding a 30 minutes pause to the beginning of the script 'works', as after the 30 minutes the subjects array is populated as the email is (99.99% of the time) sent within a 30 minute window.
However, it is definitely far from ideal as the email might be received within 15 minutes, meaning the test is 'wasting' 15 minutes.
So what I'd ideally like to do is write some form of loop (?) that tests if the subjects array is populated or not.
So if the array is populated, carry on with the rest of the test script (which entails testing that the array contains a certain text).
If the array is not populated, wait for another minute before trying again.
Continue this trying every minute until the array is populated.
I've tried setInterval, For loops, While loops, etc but I can't seem to get them to work and I'm out of ideas to be honest.
Any advice, help, references would be greatly appreciated and any more info can be promptly added if required.
One way to do that could be using recursion.
const createPromise = ms => new Promise((resolve, reject) => {
setTimeout(() => resolve(ms), ms)
});
function findUnseenEmails(connection) {
return connection.openBox('INBOX').then(function () {
var searchCriteria = [
'UNSEEN'
];
var fetchOptions = {
bodies: ['HEADER', 'TEXT'],
markSeen: false
};
return connection.search(searchCriteria, fetchOptions).then(function (results) {
var subjects = results.map(function (res) {
return res.parts.filter(function (part) {
return part.which === 'HEADER';
})[0].body.subject[0];
});
console.log(subjects);
return subjects.length > 0 ? subjects : createPromise(5000).then(function() { return findUnseenEmails(connection)});
});
});
}
imaps.connect(config).then(function (connection) {
return findUnseenEmails(connection);
}).then((subjects) => console.log('finished', subjects));
Of course there is a possibility and danger of stack overflow, but in such scenario feel free to come back to stack overflow to find here with the help of our community non-recursive solution.
Result:
EDIT:
Answering your question regarding closing connection:
I'd do it like this (in findUnseenEmails function)
if (subjects.length > 0) {
connection.end();
return subjects;
} else {
return createPromise(5000).then(function() { return findUnseenEmails(connection)});
}
Good morning! I would like to know how I can make my bot to ban words, but not just the word, I want it to ban the entire sentence that is written. I've done this, but the problem is that it doesn't ban the entire sentence.
client.on('message', message => {
if (message.content === 'BAD WORD EXAMPLE') {
message.delete({
timeout: 1,
reason: 'Mensaje eliminado, contenido inapropiado..'
});
message.channel.send(' Mensaje eliminado por contenido inapropiado');
}
})
If you want to simply ban the member that sent a message including badWords, basically you can follow #Nurfey's answer and there's much simpler code, like
const badWords = ["foo", "faz", "bar"];
client.on('message', message => {
const hasBadWord = badWords.some(banWord => message.includes(banWord))
if(hasBadWord) {
// delete the message
}
});
If your checking will be more complex so that you want to write 2+ sentences, you can also do this:
const hasBadWord = badWords.some(banWord => {
// multiple sentences here, and returns true or false
})
The full documentation of Array.some() is available on MDN.
Based on what you've written, you could try this:
const badWords = ["foo", "faz", "bar"];
client.on('message', message => {
let hasBadWord = false;
badWords.forEach(badWord => {
if(hasBadWord === false) {
if(message.includes(badWord)) hasBadWord = true; // you could do message.toLowerCase().includes(badWord) for case sensitivity
}
});
if(hasBadWord === true) {
// delete the message
}
});
it's not particularly refined, but you could optimize it if you want, this is just for making it as easily readable as I can make it
So this is moretheless in relation to the previous question, which was answered (the previous one) The bot is no longer spitting out a lot of errors whenever somebody runs the command, and the bot successfully DMs the user the response prompt, but it seems as if the Message Collector isn't starting? The bot DMs the user, nothing spits out in Console, and that's it. You can respond to the bot all day, and it wont collect it and send it to the channel ID. Any pointers?
Here's the code I believe it may revolve around:
collector.on('collect', (message, col) => {
console.log("Collected message: " + message.content);
counter++; ```
And here is all of the code (just in case it actually doesn't revolve around that):
``` if(message.content.toLowerCase() === '&reserveupdate') {
message.author.send('**Thanks for updating us on the reserve. Please enter what you are taking from the reserve below this message:**');
let filter = m => !m.author.bot;
let counter = 0;
let collector = new discord.MessageCollector(message.author, m => m.author.id, filter);
let destination = client.channels.cache.get('my channel id');
collector.on('collect', (message, col) => {
console.log("Collected message: " + message.content);
counter++;
if(counter === 1) {
message.author.send("**Thanks for updating us on the reserve, it is highly appreciated.**");
collector.stop();
}
I think you might be wrong on how you created your message collector.
According to the docs, you should make it this way :
const filter = m => !m.author.bot;
// If you are in an async function :
const channel = await message.author.createDM();
// Paste code here
// Otherwise :
message.author.createDM.then(channel => {
// Paste code here
});
// Code to paste :
const collector = channel.createMessageCollector(filter, { max: 1, time: 60000 });
collector.on('collect', msg => {
// ...
});
Hope this will help you to solve your issue ! :)