I'm working on a twitter bot that has the goal of posting two images along with a string of text. I'm using node.js (for the first time, I should probably add) and the Twit package.
I'm having a variety of issues, many of which are likely just due to me being a novice, but I genuinely can't figure out how to make the dang thing output correctly. I've managed to get text and a single image to output, but I'm trying to spit out two images at once.
The main bot chunk uses the following code to build and schedule a tweet:
function mainPostBot() {
console.log("Now assembling a new tweet.");
var leftCard = getRandomNumber(1, 36);
console.log("The left card is #" + leftCard + ", " + cardNames[leftCard] + ".");
// Generates a random number for the left card.
var leftImagePath = path.join(__dirname, '/leftImage/' + imageArray[leftCard]);
console.log("The left image's path is " + leftImagePath);
// Gives the file path to access the correct image for the left.
var rightCard = getRandomNumber(1, 36);
console.log("The right card is #" + rightCard + ", " + cardNames[rightCard] + ".");
// Generates a random number for the right card.
while (leftCard == rightCard) {
var rightCard = getRandomNumber(1, 36);
console.log("Whoops! The right card is now #" + rightCard + ", " + cardNames[rightCard] + ".");
// Generates a random number for the right card in the case of doubles.
}
var rightImagePath = path.join(__dirname, '/rightImage/' + imageArray[rightCard]);
console.log("The right image's path is " + rightImagePath);
// Gives the file path to access the correct image for the left.
console.log('Encoding the images...');
var b64contentLeft = fs.readFileSync(leftImagePath, { encoding: 'base64' });
var b64contentRight = fs.readFileSync(rightImagePath, { encoding: 'base64' });
var bothImages = (b64contentLeft + "," + b64contentRight);
// This encodes the images in base64, which twitter needs. I guess. I dunno, man.
var tweetText = (jsUcfirst(cardNames[leftCard]) + ' and ' + cardNames[rightCard] + '. (#' + leftCard + " " + cardCorrespond[leftCard] + "/#" + rightCard + " " + cardCorrespond[rightCard] + ")");
// This constructs the grammar of the tweet.
// jsUcfirst capitalizes the first letter of a string so it lets me cheat a sentence start.
var tweetTime = getRandomNumber(1000*60*60*4, 1000*60*60*24*3+1);
// Generates an amount of time before the next tweet.
sendTweet(tweetText, bothImages, tweetTime);
setTimeout(mainPostBot, tweetTime);
}
mainPostBot();
cardNames, cardCorrespond, and imageArray are just big arrays at the top of the program that list the names of the images, some info about them, and their filenames, respectively:
var cardNames = new Array(
"the Fool", //This one will never be called bc of the number generator and it's fun bc, y'know, Tarot
"the Rider","the Clover","the Ship","the House","the Tree","the Clouds","the Snake","the Coffin","the Bouquet","the Scythe","the Whip", //"the Nae Nae",
"the Birds","the Child","the Fox","the Bear","the Stars","the Stork","the Dog","the Tower","the Garden","the Mountain","the Crossroads",
"the Mice","the Heart","the Ring","the Book","the Letter","the Gentleman","the Lady","the Lily","the Sun","the Moon","the Key","the Fish",
"the Anchor","the Cross"
);
var cardCorrespond = new Array(
" ","9♥","6♦","10♠","K♥","7♥","K♣","Q♣","9♦","Q♠","J♦","J♣","7♦","J♠","9♣","10♣","6♥","Q♥","10♥",
"6♠","8♠","8♣","Q♦","7♣","J♥","A♣","10♦","7♠","A♥","A♠","K♠","A♦","8♥","8♦","K♦","9♠","6♣"
);
var imageArray = new Array(
" ","01.png","02.png","03.png","04.png","05.png","06.png","07.png","08.png","09.png","10.png","11.png","12.png","13.png",
"14.png","15.png","16.png","17.png","18.png","19.png","20.png","21.png","22.png","23.png","24.png","25.png","26.png",
"27.png","28.png","29.png","30.png","31.png","32.png","33.png","34.png","35.png","36.png"
);
And once mainPostBot has the tweet fully constructed, it's delivered to sendTweet:
function sendTweet(text, images, time){
console.log('Uploading the images...');
T.post('media/upload', { media_data: images }, function (err, data, response){
if (err){
console.log("There's an issue uploading the images.");
console.log(err);
} else {
console.log('Images uploaded!');
console.log("Now tweeting...")
T.post('statuses/update', {
status: text,
media_ids: new Array(data.media_id_string)
}, function(err, data, response){
if (err) {
console.log("An error has occurred during posting.");
console.log(err);
} else {
console.log("Post successful!");
console.log("The tweet says:" + text);
console.log("The next tweet will send in " + msToTime(time) + "!");
}
});
}
});
}
Any ideas? I'm open to using other npm packages, for sure, but I just can't figure out why this doesn't work as it is. Thanks for reading, and let me know if you need any other bits of the code.
EDIT 1: My roommate who also dabbles in this sort of stuff found a potentially useful link on github for another package, node-twitter. In that link, a poster explains that the images should be delivered as a string, separated by commas, so I added some edits to mainPostBot and sendTweet, mostly in the passing of b64 image data.
EDIT 2: Those edits are now reflected in the code above, as well as some other fixes I've made to the project as a whole. I got to a point where things are running smoothly again (found a missing bracket, I suck at this coding stuff), and there's tweets posting successfully, but just as before I'm not getting the second image through. Roommate who helped earlier is suggesting to just pump out static single images for every possible card combination but there's gotta be a more elegant solution. Again, any ideas could save a week of my weird bedroom tinkering, and I appreciate any eyes on this.
It took a lot of tinkering, but I figured it out. Each image has to be uploaded to twitter individually, so after loading the image, I save it's data.media_id_string to a variable, and then load those values into the tweet in an array.
I've removed the line from mainPostBot where I combined b64contentLeft and b64contentRight and added it into the sendTweet code, using the returned data strings. Now, I call sendTweet() with:
sendTweet(tweetText, b64contentLeft, b64contentRight, tweetTime);
And sendTweet() is now looking like this:
function sendTweet(text, leftimage, rightimage, time){
console.log('Uploading the images...');
T.post('media/upload', { media_data: leftimage }, function (err, data, response){
if (err){
console.log("There's an issue uploading the left image.");
console.log(err);
} else {
console.log('Left image uploaded!');
var leftID = data.media_id_string;
T.post('media/upload', { media_data: rightimage }, function (err, data, response){
if (err){
console.log("There's an issue uploading the right image.");
console.log(err);
} else {
console.log('Right image uploaded!');
var rightID = data.media_id_string;
var bothImages = ( leftID + "," + rightID );
console.log("Now tweeting...")
T.post('statuses/update', {
status: text,
media_ids: new Array(bothImages)
}, function(err, data, response){
if (err) {
console.log("An error has occurred during posting.");
console.log(err);
} else {
console.log("Post successful!");
console.log("The tweet says: " + text);
console.log("The next tweet will send in " + msToTime(time) + "!");
}
});
}
});
}
});
}
Essentially, if the left image uploads correctly, it'll save that ID, then try for the right image. IF that's successful, it'll save that ID as well, then combine the two into a string separated with a comma, which is loaded into the media_ids array as bothImages.
This was kind of a nightmare to solve, but I wanted to make sure it's documented in case anyone else stumbles here looking for the same answer.
I am currently having an issue with using JQuery with json data to append a calculated amount of <li> elements to a <ul>. Here is my code:
$.getJSON("http://api.hivemc.com/v1/game/timv", function(data) {
$.each(data.achievements, function(key,value){
var unlocked = "Locked";
$.each(maindata.achievements, function(key2,value2){
if(value.name == key2){
unlocked = "Unlocked";
}
});
$("#achs").append("<li><p>" + value.publicname + "</p><span>"+ unlocked + "</span></li>");
});
});
As you can see, I am getting JSON data from a URL. In this, there is an array achievements. The variable maindata was set earlier from another $.getJSON().
For each achievement, I have to append a <li> element with the data of the achievement to a <ul> which has the id of #achs. However, in order to see if the achievement is unlocked, I have to check for the achievements name in the maindata JSON, meaning another $.each() loop inside the current $.each() loop.
Without the extra loop, the code works fine, and successfully forms a list of achievements and whether they are unlocked or not. However, whenever I add the extra $.each() back in again, it only works when I reload the page or go back and back onto it again.
Does anyone know why this is happening? Any help would be greatly appreciated as I am sure you can see I am not very experienced with JQuery. Also, I have been able to do this with just PHP, retrieving JSON data, but I wanted to see if using JQuery would be quicker loading than PHP.
The maindata JSON is retrieved from this code:`var maindata;
$.getJSON("http://api.hivemc.com/v1/player/" + $user + "/timv", function(data) {
$('#1').text(data.total_points);
$('#2').text(data.i_points);
$('#3').text(data.t_points);
$('#4').text(data.d_points);
$('#5').text(data.role_points);
$('#6').text(data.most_points);
maindata = data;
if(data.detectivebook == true)
$('#7').text("Yes");
else
$('#7').text("No");
$flare = data.active_flareupgrade;
$flare = $flare.charAt(0).toUpperCase() + $flare.slice(1).toLowerCase();
$('#8').text($flare);
$('#9').text(data.title);
var d = new Date(data.lastlogin * 1000);
var n = d.toISOString();
$('#10').text(d.getDate() + "/" + d.getMonth() + "/" + d.getFullYear());
$.getJSON("http://api.hivemc.com/v1/game/timv", function(data2) {
$.each(data2.achievements, function(key,value){
var unlocked = "Locked";
$.each(maindata.achievements, function(key2,value2){
if(value.name == key2){
unlocked = "Unlocked";
}
});
$("#achs").append("<li><p>" + value.publicname + "</p><span>"+ unlocked + "</span></li>");
});
});
});`
Thanks.
You should wait for your earlier AJAX call to complete before doing any processing that depends on the results of both calls:
var promise1 = $.getJSON(...); // get maindata
var promise2 = $.getJSON(...); // get data
$.when(promise1, promise2).then(function(maindata, data) {
// do your processing here
...
});
NB: there's no need to supply callbacks to the $.getJSON calls - do the processing within the .then callback.
So,I am trying to use the twitch API:
https://codepen.io/sterg/pen/yJmzrN
If you check my codepen page you'll see that each time I refresh the page the status order changes and I can't figure out why is this happening.
Here is my javascript:
$(document).ready(function(){
var ur="";
var tw=["freecodecamp","nightblue3","imaqtpie","bunnyfufuu","mushisgosu","tsm_dyrus","esl_sc2"];
var j=0;
for(var i=0;i<tw.length;i++){
ur="https://api.twitch.tv/kraken/streams/"+tw[i];
$.getJSON(ur,function(json) {
$(".tst").append(JSON.stringify(json));
$(".name").append("<li> "+tw[j]+"<p>"+""+"</p></li>");
if(json.stream==null){
$(".stat").append("<li>"+"Offline"+"</li>");
}
else{
$(".stat").append("<li>"+json.stream.game+"</li>");
}
j++;
})
}
});
$.getJSON() works asynchronously. The JSON won't be returned until the results come back. The API can return in different orders than the requests were made, so you have to handle this.
One way to do this is use the promise API, along with $.when() to bundle up all requests as one big promise, which will succeed or fail as one whole block. This also ensures that the response data is returned to your code in the expected order.
Try this:
var channelIds = ['freecodecamp', 'nightblue3', 'imaqtpie', 'bunnyfufuu', 'mushisgosu', 'tsm_dyrus', 'esl_sc2'];
$(function () {
$.when.apply(
$,
$.map(channelIds, function (channelId) {
return $.getJSON(
'https://api.twitch.tv/kraken/streams/' + encodeURIComponent(channelId)
).then(function (res) {
return {
channelId: channelId,
stream: res.stream
}
});
})
).then(function () {
console.log(arguments);
var $playersBody = $('table.players tbody');
$.each(arguments, function (index, data) {
$playersBody.append(
$('<tr>').append([
$('<td>'),
$('<td>').append(
$('<a>')
.text(data.channelId)
.attr('href', 'https://www.twitch.tv/' + encodeURIComponent(data.channelId))
),
$('<td>').text(data.stream ? data.stream.game : 'Offline')
])
)
})
})
});
https://codepen.io/anon/pen/KrOxwo
Here, I'm using $.when.apply() to use $.when with an array, rather than list of parameters. Next, I'm using $.map() to convert the array of channel IDs into an array of promises for each ID. After that, I have a simple helper function with handles the normal response (res), pulls out the relevant stream data, while attaching the channelId for use later on. (Without this, we would have to go back to the original array to get the ID. You can do this, but in my opinion, that isn't the best practice. I'd much prefer to keep the data with the response so that later refactoring is less likely to break something. This is a matter of preference.)
Next, I have a .then() handler which takes all of the data and loops through them. This data is returned as arguments to the function, so I simply use $.each() to iterate over each argument rather than having to name them out.
I made some changes in how I'm handling the HTML as well. You'll note that I'm using $.text() and $.attr() to set the dynamic values. This ensures that your HTML is valid (as you're not really using HTML for the dynamic bit at all). Otherwise, someone might have the username of <script src="somethingEvil.js"></script> and it'd run on your page. This avoids that problem entirely.
It looks like you're appending the "Display Name" in the same order every time you refresh, by using the j counter variable.
However, you're appending the "Status" as each request returns. Since these HTTP requests are asynchronous, the order in which they are appended to the document will vary each time you reload the page.
If you want the statuses to remain in the same order (matching the order of the Display Names), you'll need to store the response data from each API call as they return, and order it yourself before appending it to the body.
At first, I changed the last else condition (the one that prints out the streamed game) as $(".stat").append("<li>"+jtw[j]+": "+json.stream.game+"</li>"); - it was identical in meaning to what you tried to achieve, yet produced the same error.
There's a discrepancy in the list you've created and the data you receive. They are not directly associated.
It is a preferred way to use $(".stat").append("<li>"+json.stream._links.self+": "+json.stream.game+"</li>");, you may even get the name of the user with regex or substr in the worst case.
As long as you don't run separate loops for uploading the columns "DisplayName" and "Status", you might even be able to separate them, in case you do not desire to write them into the same line, as my example does.
Whatever way you're choosing, in the end, the problem is that the "Status" column's order of uploading is not identical to the one you're doing in "Status Name".
This code will not preserve the order, but will preserve which array entry is being processed
$(document).ready(function() {
var ur = "";
var tw = ["freecodecamp", "nightblue3", "imaqtpie", "bunnyfufuu", "mushisgosu", "tsm_dyrus", "esl_sc2"];
for (var i = 0; i < tw.length; i++) {
ur = "https://api.twitch.tv/kraken/streams/" + tw[i];
(function(j) {
$.getJSON(ur, function(json) {
$(".tst").append(JSON.stringify(json));
$(".name").append("<li> " + tw[j] + "<p>" + "" + "</p></li>");
if (json.stream == null) {
$(".stat").append("<li>" + "Offline" + "</li>");
} else {
$(".stat").append("<li>" + json.stream.game + "</li>");
}
})
}(i));
}
});
This code will preserve the order fully - the layout needs tweaking though
$(document).ready(function() {
var ur = "";
var tw = ["freecodecamp", "nightblue3", "imaqtpie", "bunnyfufuu", "mushisgosu", "tsm_dyrus", "esl_sc2"];
for (var i = 0; i < tw.length; i++) {
ur = "https://api.twitch.tv/kraken/streams/" + tw[i];
(function(j) {
var name = $(".name").append("<li> " + tw[j] + "<p>" + "" + "</p></li>");
var stat = $(".stat").append("<li></li>")[0].lastElementChild;
console.log(stat);
$.getJSON(ur, function(json) {
$(".tst").append(JSON.stringify(json));
if (json.stream == null) {
$(stat).text("Offline");
} else {
$(stat).text(json.stream.game);
}
}).then(function(e) {
console.log(e);
}, function(e) {
console.error(e);
});
}(i));
}
});
I have been searching for many hours over several days for this answer and though there are many topics on how to include files in a project (also here at Stack Overflow), I have not yet found THE solution to my problem.
I'm working on a project where I want to include one single object at a time, from many different files (I do not want to include the files themselves, only their content). All the object in all the files have the same name, only the content is different.
It is important that I do not get a SCRIPT tag in the head section of the page as all the content from the files will have the same names. None of the files will have functions anyways, only one single object, that will need to be loaded one at the time and then discarded when the next element is loaded.
The objects will hold the data that will be shown on the page and they will be called from the menu by an 'onclick' event.
function setMenu() // The menu is being build.
{
var html = '';
html += '<table border="0">';
for (var i = 0; i<menu.pages.length; i++)
{
html += '<tr class="menuPunkt"><td width="5"></td><td onclick="pageName(this)">'+ menu.pages[i] +'</td><td width="5"></td></tr>';
}
// menu is a global object containing elements such as an array with
// all the pages that needs to be shown and styling for the menu.
html += '</table>';
document.getElementById("menu").innerHTML = html;
style.setMenu(); // The menu is being positioned and styled.
}
Now, when I click on a menu item the pageName function is triggered and I'm sending the HTML element to the function as well, it is here that I want the content from my external file to be loaded into a local variable and used to display content on the page.
The answer I want is "How to load the external obj into the function where I need it?" (It may be an external file, but only in the term of not being included in the head section of the project). I'm still loading the the file from my own local library.
function pageName(elm) // The element that I clicked is elm.
{
var page = info.innerHTML; // I need only the innerHTML from the element.
var file = 'sites/' + page + '.js'; // The file to be loaded is created.
var obj = ?? // Here I somehow want the object from the external file to be loaded.
// Before doing stuff the the obj.
style.content();
}
The content from the external file could look like this:
// The src for the external page: 'sites/page.js'
var obj = new Object()
{
obj.innerHTML = 'Text to be shown';
obj.style = 'Not important for problem at hand';
obj.otherStuff = ' --||-- ';
}
Any help will be appreciated,
Molle
Using the following function, you can download the external js file in an ajax way and execute the contents of the file. Please note, however, that the external file will be evaluated in the global scope, and the use of the eval is NOT recommended. The function was adopted from this question.
function strapJS(jsUrl) {
var jsReq = (window.XMLHttpRequest) ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP");
if (jsReq === null) {
console.log("Error: XMLHttpRequest could not be initiated.");
}
jsReq.onload = function () {
try {
eval(jsReq.responseText);
} catch (e) {
console.log("Error: The script file contains errors." + e);
}
};
try {
jsReq.open("GET", jsUrl, true);
jsReq.send(null);
} catch (e) {
console.log("Error: Cannot retrieving data." + e);
}
}
JSFiddle here
Edit: 1
After some refactoring, I came up with this:
function StrapJs(scriptStr, jsObjName) {
var self = this;
self.ScriptStr = scriptStr;
self.ReturnedVal = null;
function _init() {
eval(self.ScriptStr);
self.ReturnedVal = eval(jsObjName);
}
_init();
}
You can then get the script string any way you want and just instantiate a new StrapJs object with the script string and name of the object to return inside the script string. The ReturnedVal property of the StrapJs object will then contain the object you are after.
Example usage:
var extJS = "var obj = " +
"{ " +
" innerHTML : 'Text to be shown', " +
" style : 'Not important for problem at hand', " +
" otherStuff : ' --||-- ' " +
"}; ";
var extJS2 = "var obj = " +
"{ " +
" innerHTML : 'Text to be shown 2', " +
" style : 'Not important for problem at hand 2', " +
" otherStuff : ' --||-- 2' " +
"}; ";
var strapJS = new StrapJs(extJS, 'obj');
var strapJS2 = new StrapJs(extJS2, 'obj');
console.log(strapJS.ReturnedVal.innerHTML);
console.log(strapJS2.ReturnedVal.innerHTML);
See it in action on this fiddle
Update: Solved, it was indeed a scope issue. I got around it by moving the user list code inside the database class and returning the prebuilt list.
Using node.js, I make an asynchronous call to function findUser and I build a list of users from the callback into the variable content. This works fine during the loop (where it says content variable is available) but when the loop exits, the variable is empty. How can I rewrite the code so that the value of variable content is available outside the loop?
exports.listUsers=function(req,res) {
var content=''
isLoggedIn=user.findUser({},function(myuser) {
content = content +'<li>' + myuser.firstname + ' ' + myuser.lastname + "</li>\n";
//here value of content var is available
console.log(content)
})
//here value of content var is empty
console.log(content)
showPage(req,res,{'content':content})
}
If findUser() is asynchronous (which you indicate), then the issue is that findUser() has not yet completed when showPage() is called.
For asynchronous functions, you can only use their results from the success handler function. You can't call an asynchronous function and expect to use it synchronously like your current code.
I don't know exactly what you're trying to accomplish, but this is the general design pattern you would need to use:
exports.listUsers=function(req,res) {
isLoggedIn=user.findUser({},function(myuser) {
var content = '<li>' + myuser.firstname + ' ' + myuser.lastname + "</li>\n";
//here value of content var is available
console.log(content)
showPage(req,res,{'content':content})
});
}
Or, if the callback is being called many times once for each user, you can accumulate the content and call showPage() on the last callback:
exports.listUsers=function(req,res) {
var content = "";
isLoggedIn=user.findUser({},function(myuser) {
content += '<li>' + myuser.firstname + ' ' + myuser.lastname + "</li>\n";
//here value of content var is available
console.log(content)
// devise some logic to know when the last callback is being called
// perhaps based on a user count
if (this is the last user callback) {
showPage(req,res,{'content':content})
}
});
}