How to split text depending on word count - node.js

I am trying to make a lyric project using discord.js, cheerio and the website called genius.com.
I have successfully found a way to scrape the lyrics from the website, I am onto the part where I need to split it because discord has a max word limit of 2000.
I can check how many characters/words are in the overall lyrics by doing lyrics.length, I just need to find a way to split the string and send both, in the future I might implement richEmbeds to make it more stylish but for now I'm focusing on the basics.
var request = require('request');
var cheerio = require('cheerio');
/*
This is a project for my discord bot, the reason for the 2000 word limit is because
discords character limit is currently set to 2000, this means that i will have to add
a function to split the lyrics and send each part
*/
//Define the URL that we are going to be scraping the data from
var UR_L = "https://genius.com/Josh-a-and-jake-hill-not-afraid-of-dying-lyrics";
//send a request to the website and return the contents of the website
request(UR_L, function(err, resp, body) {
//load the website using cheerio
$ = cheerio.load(body);
//define lyrics as the selector to text form
var lyrics = $('p').text();
if (lyrics.length > "2000" && lyrics.length < "4000") {
} else if (lyrics.length > "4000" && lyrics.length < "6000") {
} else {
//send the lyrics as one message
}
})
You can find a live version running here on repl.it.

You don't need to use any fancy function, that function is already built in discord.js: you can attach some options to a message, and MessageOptions.split is what you're searching for. When you want to send the text, do it like this:
channel.send(lyrics, { split: true });
If lyrics.length is greater that the limit, discord.js will cut your messages and send them one after the other, making it seem like it's only one.
channel is the TextChannel you want to send the messages to.

Discord has a 2000 characters limit not a 2000 words limit.
One solution to your problem could be this:
// This will result in an array with strings of max 2000 length
const lyricsArr = lyrics.match(/.{1,2000}/g);
lyricsArr.forEach(chunk => sendMessage(chunk))
Given the async nature of sending messages, you might want to look into modules like p-iteration to ensure the chunks arrive in the correct order.
That being said, there exists APIs for getting lyrics of songs, which I would recommend instead of scraping. See apiseeds lyrics API as an example.
UPDATE
const lyrics = 'These are my lyrics';
const lyricsArr = lyrics.match(/.{1,8}/g);
console.log(lyricsArr); // [ 'These ar', 'e my lyr', 'ics' ]
lyricsArr.forEach((chunk, i) => {
// Break if this is the last chunk.
if (i == lyricsArr.length -1) {
return;
}
// If last character is not a space, we split a word in two.
// Add additional non-wordbreaking symbols between the slashes (in the regex) if needed.
if (!chunk[chunk.length - 1].match(/[ ,.!]/)) {
const lastWord = chunk.match(/\s([^ .]+)$/)
lyricsArr[i + 1] = lastWord[1] + lyricsArr[i + 1];
lyricsArr[i] = lyricsArr[i].split(/\s[^ .]*$/)[0];
}
})
console.log(lyricsArr) // [ 'These', 'are my', 'lyrics' ]
Updated as per the comments.
This is some crude code that i did not spend much time on, but it does the job.
Some info when using this approach:
You need to add any symbols that should not be considered wordbreaking to the regex in the second if
This has not been tested thoroughly, so use at your own risk.
It will definitely break if you have a word in the lyrics longer than the chunk size. Since this is around 2000, I imagine it will not be problem.
This will no longer ensure that the chunk length is below the limit, so change the limit to around 1900 to be safe

You can use .split( ) Javascript function.
word_list = lyrics.split(" ")
And word_list.length to access the number of words in your message and word_list[0] to select the first word for instance.

Related

Bot won't react after seeing a word contained in a word list

I am trying to make my bot react to every message containing words like hey or hi, but when I send a message containing one of those, it does nothing, here is my code:
const words = [
'hey',
'Hey'
]
client.on('message', () => {
if (message.content.includes(words)) return message.react("👋")
})
There are no errors in the console either.
Issue
message.content.includes(words)
is equivalent to
message.content.includes(words.toString())
which actually checks if the string joining the words array by a comma exists in message.content (not what you want). In your case, it does the following:
message.content.includes('hey,Hey') // words = ['hey', 'Hey']
Array.prototype.toString returns a string joining the array elements by a comma.
Solution
You can use Array.prototype.some to check if any of the words in the words array is present in the message content. And you don't have to add the capitalized word forms to words. It's enough to only add lowercase words and use String.prototype.toLowerCase to lower case the message content before checking if any of the entries in words exists in the content.
const words = [
'hey',
// ...
]
const content = message.content.toLowerCase()
const shouldReact = words.some((word) => content.includes(word))
if (shouldReact) {
// ...
}

How to use a loop inside message.react() inside async function discord.js

I am making a message that allows the user to target another user via a Discord reaction for a game I'm making compatible with Discord. I am trying to make this work for any number of players without specifying each possible amount of players. I have been using a loop to try and make this work. I just want the bot to add a reaction of 1, 2 and 3 as options for a game that has 3 total players (or users) and display the correct 1, 2 or 3 emoji I have specified in reaction_numbers below. (Those emoji's are just blue squares with the number in them that I know work with Discord reactions)
I get an error of (node:10988) UnhandledPromiseRejectionWarning: TypeError: Emoji must be a string or Emoji/ReactionEmoji
var reaction_numbers = ["\u0030\u20E3","\u0031\u20E3","\u0032\u20E3","\u0033\u20E3","\u0034\u20E3","\u0035\u20E3", "\u0036\u20E3","\u0037\u20E3","\u0038\u20E3","\u0039\u20E3"]
var PlayerListMessage = [] <<< Gets list of players and arranges them
for (let i = 0; i < playerUserArray.length; i++) {
PlayerListMessage.push(`${i+1}: ${playerUserArray[i]}\n`)
}
async function QuestionPlayerToTarget(){
let msg = await message.author.send(`Which player activated the card you would like to negate?\n${PlayerListMessage.join("")}\nPlease select only one player.`)
for (var i of playerUserArray){
await msg.react(reaction_numbers[i+(1)]) <<< Error happens here.
}
const filter = (reaction, user) => {
return [reaction_numbers[1], reaction_numbers[2], reaction_numbers[3], reaction_numbers[4], reaction_numbers[5], reaction_numbers[6]].includes(reaction.emoji.name) && user.id === message.author.id;
};
const reply = await msg.awaitReactions(filter, { max: 1 })
.catch(console.error);
const targetPlayer = reply.first()
return targetPlayer
}
var targetPlayer = await QuestionPlayerToTarget()
console.log(targetPlayer)
Any ideas on how to make this loop add reactions for the exact number of players in the game? Thanks in advance for the help!
Next time please comment with # not <<<
As the error says: Emoji must be a string or Emoji/ReactionEmoji > string or emoji.
How to use msg.react()
So you can either use msg.react("emoji") or msg.react(msg.guild.emojis.get("emojiid")).
The first option is for already existing emojis, like numbers in blue squares.
So in Discord you can put a \ before you post an emoji and you are getting this emoji as string.
The second option is for guild-emojis, emojis, which are only available in this guild.
In your Case
use http://getemoji.com/ and search for "one" and you'll get 1️⃣. You can use this symbol for msg.react("1️⃣").
So just put this in the list reaction_numbers and so on...
I wasn't too far off in my attempts. I ended up changing the loop type from a "for of" loop to a standard for loop and it worked as expected. Code changes as follows.
for (let i = 1; i < playerUserArray.length; i++) {
await msg.react(reaction_numbers[i])
}
var reaction_numbers = ["\u0031\u20E3","\u0032\u20E3","\u0033\u20E3","\u0034\u20E3","\u0035\u20E3", "\u0036\u20E3","\u0037\u20E3","\u0038\u20E3","\u0039\u20E3", "\u0030\u20E3"]
I also took the first value of reaction_numbers and put it at the end of the list in order to make index value 0 = emoji of the number 1 with the blue square.

baconjs: throttle consecutive events with criteria

I'm coding a messaging app with Node.js and I need to detect when the same user sends N consecutive messages in a group (to avoid spammers). I'm using a bacon.js Bus where I push the incoming messages from all users.
A message looks like this:
{
"text": "My message",
"user": { "id": 1, name: "Pep" }
}
And this is my working code so far:
const Bacon = require('baconjs')
const bus = new Bacon.Bus();
const CONSECUTIVE_MESSAGES = 5;
bus.slidingWindow(CONSECUTIVE_MESSAGES)
.filter((messages) => {
return messages.length === MAX_CONSECUTIVE_MESSAGES &&
_.uniqBy(messages, 'user.id').length === 1;
})
.onValue((messages) => {
console.log(`User ${__.last(messages).user.id}`);
});
// ... on every message
bus.push(message);
It creates a sliding window, to keep only the number on consecutive messages I want to detect. On every event, it filters the array to let the data flow to the next step only if all the messages in the window belong to the same user. Last, in the onValue, it takes the last message to get the user id.
The code looks quite dirty/complex to me:
The filter doesn't look very natural with streams. Is there a better way to emit an event when N consecutive events match some criteria? .
Is there a better way to receive just a single event with the user (instead of an array of messages) in the onValue function.
It doesn't really throttle. If a user sends N messages in one year, he or she shouldn't be detected. The stream should forget old events somehow.
Any ideas to improve it? I'm open to migrating it to rxjs if that helps.
Maybe start with
latestMsgsP = bus.slidingWindow(CONSECUTIVE_MESSAGES)
.map(msgs => msgs.filter(msg => msgAge(msg) < AGE_LIMIT))
See if we should be blockking someone
let blockedUserIdP = latestMsgsP.map(getUserToBlock)
Where you can use something shamelessly imperative such as
function getUserToBlock(msgs) {
if (msgs.length < CONSECUTIVE_MESSAGES) return
let prevUserId;
for (var i = 0; i < msgs.length; i++) {
let userId = msgs[i].user.id
if (prevUserId && prevUserId != userId) return
prevUserId = userId
}
return prevUserId
}
Consider mapping the property you’re interested in as early as possible, then the rest of the stream can be simpler. Also, equality checks on every item in the sliding window won’t scale well as you increase the threshold. Consider using scan instead, so you simply keep a count which resets when the current and previous values don’t match.
bus
.map('.user.id')
.scan([0], ([n, a], b) => [a === b ? n + 1 : 1, b])
.filter(([n]) => n >= MAX_CONSECUTIVE_MESSAGES)
.onValue(([count, userId]) => void console.log(`User ${userId}`));

synchronous execution of database function in node.js

There are two collections products and stock in my database.Each product have multipple stock with different supplier and attributes of products.
I have selected the products from products collection and run a loop for each product. I need to append price and offer price from the stock collection to products i have selected already.
I think the for loop compleated its execution before executiong the find method on stock collection.I need to execute everything in the loop in serial manner(not asynchronous). Please check the code below and help me to solve this. I'm new in node.js and mongodb
collection = db.collection('products');
collection.find().toArray(function(err, abc) {
var finalout = [];
for( var listProducts in abc){
var subfinal = {
'_id' :abc[listProducts]['_id'],
'min_price' :abc[listProducts]['value']['price'],
'stock' :abc[listProducts]['value']['stock'],
'name' :abc[listProducts]['value']['name'],
'price' :'',
'offer_price' :'',
};
collection = db.collection('stock');
collection.find({"product":abc[listProducts]['_id'] ,"supplier":abc[listProducts]['value']['def_supplier']}).toArray(function(err, newprod) {
for( var row in newprod){
subfinal['price'] = newprod[row]['price'];
subfinal.offer_price = newprod[row]['offer_price'];
}
finalout.push(subfinal);
});
}
console.log(finalout);
});
Yes, the loop is running and starting the get requests on the database all at the same time. It is possible, like you said, to run them all sequentially, however it's probably not what you're looking for. Doing it this way will take more time, since each request to Mongo would need to wait for the previous one to finish.
Considering that each iteration of the loop doesn't depend on the previous ones, all you're really looking for is a way to know once ALL of the operations are finished. Keep in mind that these can end in any order, not necessarily the order in which they were initiated in the loop.
There's was also an issue in the creation of the subfinal variable. Since the same variable name is being used for all iterations, when they all come back they'll be using the final assignment of the variable (all results will be written on the same subfinal, which will have been pushed multiple times into the result). To fix that, I've wrapped the entire item processing iteration into another function to give each subfinal variable it's own scope.
There are plenty of modules to ease this kind of management, however here is one way to run some code only after all the Mongo calls are finished using a counter and callback that doesn't require any additional installs:
var finished = 0; // incremented every time an iteration is done
var total = Object.keys(abc).length; // number of keys in the hash table
collection = db.collection('products');
collection.find().toArray(function(err, abc) {
var finalout = [];
for( var listProducts in abc){
processItem(listProducts);
}
});
function processItem(listProducts) {
var subfinal = {
'_id' :abc[listProducts]['_id'],
'min_price' :abc[listProducts]['value']['price'],
'stock' :abc[listProducts]['value']['stock'],
'name' :abc[listProducts]['value']['name'],
'price' :'',
'offer_price' :'',
};
collection = db.collection('stock');
collection.find({"product":abc[listProducts]['_id'] ,"supplier":abc[listProducts]['value']['def_supplier']}).toArray(function(err, newprod) {
for( var row in newprod){
subfinal['price'] = newprod[row]['price'];
subfinal.offer_price = newprod[row]['offer_price'];
}
finalout.push(subfinal);
finished++;
if (finished === total) { // if this is the last one to finish.
allFinished(finalout);
}
});
}
function allFinished(finalout) {
console.log(finalout);
}
It could be written more concisely with less variables, but it should be easier to understand this way.

Sharepoint > Which characters are *not* allowed in a list?

I've tried looking this up and haven't come up with the answer I'm looking for; I've found what cannot be included in filenames, folder names, and site names... but nothing on actual fields in a list.
I noticed that the percent symbol (%) is one that's not allowed in files/sites/folders. But it also doesn't populate when I try to pro grammatically add the fields to the list. I am doing this by using a small C# application that sends the data via Sharepoint 2010's built-in web services. I can manually enter the character, but it messes up each field in the row if I try it through code.
I've tried some of the escape characters that I've found via Google (_x26), but these don't seem to work either. Has anyone else had an issue with this? If these characters are allowed, how can I escape them when sending the data through a web service call?
Thanks in advance!
Justin
Any characters that aren't allowed when you enter a field name get encoded in the internal name. The format is a little different to what you show - try "_x0026_".
I usually avoid issues with weird internal names by creating the field with no spaces or special characters in the name, then renaming it. When you rename a field, only the display name changes and you keep the simple internal name.
Characters not allowed in SharePoint file name:
~, #, %, & , *, {, }, \, :, <, >, ?, /, |, "
Pasted from http://chrisbarba.com/2011/01/27/sharepoint-filename-special-characters-not-allowed/
Am I saying something weird when I state that there usually is a reason for certain characters not being allowed. I don't know which or why, but there probably is a reason.
Since you control which fields need to be there you can also dictate their (internal) names. I'd say follow best practice and name your fields using Camel case. And because you created them, you can just map the fields to the according fields in your data source.
As a follow on to #Elisa's answer, here's some JavaScript / TypeScript code that helps to prevent users from uploading files into SharePoint with invalid characters in the file name implemented on Nintex forms.
Here's the gist of the JavaScript version (note you'll have to obviously adapt for your own needs since this was designed for Nintex) :
//------------------------------------------------------------------------
//JavaScript Version:
//Code from http://cc.davelozinski.com/tips-techniques/nintex-form-tips-techniques/javascript-typescript-for-nintex-forms-to-validate-file-names
//------------------------------------------------------------------------
function validateAttachmentNames(eventObject) {
var textbox$ = NWF$(this);
var attachrowid = this.id.substring(10, 47);
var fileUploadid = attachrowid;
var index = attachrowid.substring(36);
//console.log('index:' + index);
//console.log('attachrowid:' + attachrowid);
//console.log('fileUploadid:' + fileUploadid);
if (index == '') {
attachrowid += '0';
}
var fileName = NWF.FormFiller.Attachments.TrimWhiteSpaces(textbox$.val().replace(/^.*[\\\/]/, ''));
var match = (new RegExp('[~#%\&{}+\|]|\\.\\.|^\\.|\\.$')).test(fileName);
if (match) {
isValid = false;
setTimeout(function () {
NWF$("tr[id^='attachRow']").each(function () {
var arrParts = (NWF$(this).find(".ms-addnew")[0]).href.split('"');
var fileName = arrParts[5];
var attachRow = arrParts[1];
var fileUpload = arrParts[3];
var match = (new RegExp('[~#%\&{}+\|]|\\.\\.|^\\.|\\.$')).test(fileName);
if (match) {
console.log(fileName);
NWF.FormFiller.Attachments.RemoveLocal(attachRow, fileUpload, fileName);
alert('Invalid file: ' + fileName + ' You cannot attach files with the following characters ~ # % & * { } \ : < > ? / + | \n\nThe file has been removed.');
}
});
}, 500);
}
}

Resources