Steem-JS Following Feed Voting Bot, featuring html GUI and dynamic blacklist #4

Words
1312
Reading
6 min
9y
Listen to this post
~9 min

The first three tutorials of this voting bot:

What Will You Learn?

  • How to create a voting trail that can vote based on anyone else's votes.
  • Various Steem-JS options that can obtain voted-on posts of specific accounts.
  • How to dynamically back-track an account's history with Steem-JS.
  • Rebuild some functionality so that voting power is checked for each vote, the blacklist is checked independently, and the vote qualifying post collection is separated from the voting functionality.

Requirements

  • The index.html, main.js, and main.css files covered in the first tutorials linked at the top.
  • Steem-JS
  • A text editor of your choice (I use Brackets).

Difficulty

  • Basic

Tutorial Contents

Before I get into the trail functionality, first I would like to show some slight changes into the html and CSS files which make up the web GUI. Since I will be adding a trail account, I want to be able to input whichever account I want in our control panel as well as another input to control how long to back-track account history but more on that later. Here is what I will add into the "Control Panel" fieldset:

index.html

<li>
    <label>Trail Account:</label>
    <input id="TAccount" value="curie"></input>
</li>
<li>
    <label>Trail Backtrack Time in Minutes:</label>
    <input id="TrailTime" value="1"></input>
</li>

These changes are in the same format as the rest of the Control Panel fieldset, but the next change in the "Main Display" fieldset will require a little bit more change. To vote on a trail, I will add a button specifically for that and would like to format the buttons so that I can control where they are and their size together. I will also transition from using the disabled attribute in css to check if a system is running, to class switching. These changes will require small adjustments to the index.html, main.js, and the main.css

index.html

<div class="buttonCon">
    <input id="timerStarter" class="timerb_off" type="button" value="Voting System Off"></input>
    <input id="timerTrail" class="timerb_off" type="button" value="Trailing Off"></input>
    <input id="timerStopper" type="button" value="Stop Voting System" disabled></input>
</div>

Originally, there were two buttons and they were in li elements but to make visual control easier, I will put all buttons inside a div element with the class "buttonCon". I'll also add a class to depict whether the button's system is on or off, with "timerb_off" and "timerb_on" to make CSS control more efficient. Here is the entire CSS file to compare the differences easily:

main.css

fieldset {
  float: left;
  clear: left;
  width: 100%;
  margin: 0 0 1.5em 0;
  padding: 0;
}

legend {
  margin-left: 1em;
  color: #000000;
  font-weight: bold;
}

fieldset ol {
  padding: 1em 1em 0 1em;
  list-style: none;
}

fieldset li {
  float: left;
  clear: left;
  width: 100%;
  padding-bottom: 1em;
}

fieldset div input {
  width: 100%;
  margin-bottom: 1em;
}

label {
  float: left;
  width: 14em;
  margin-right: 1em;
}

.buttonCon {
    float: left;
    clear: left;
    width: 8.5em;
    margin-left: 7.7em;
}

.timerb_on {
    background-color: forestgreen;
    color: white;
    border: 2px solid darkslateblue;
    width: 100%;
}

.timerb_off {
    background-color: firebrick;
    color: white;
    border: 2px solid darkslateblue;
    width: 100%;
}

#timerStopper {
    width: 100%;
}

This will let us control the position and size by referencing the container class with .buttonCon and I've also added some margin padding to inputs within divs. The last thing to do is the button controls in the main.js:

main.js

window.addEventListener('DOMContentLoaded', function () {
    
    document.getElementById('timerStarter').onclick = function() {
        document.getElementById('timerTrail').disabled = true;
        document.getElementById('timerStarter').disabled = true;
        document.getElementById('timerStarter').setAttribute('class', 'timerb_on');
        
        checkVotingPower();
        timerRef = setInterval(checkVotingPower, 55000); // 60000 = one minute
        
        document.getElementById('timerStarter').value = 'Voting System On';
        document.getElementById('timerStopper').disabled = false;
    };
    
    document.getElementById('timerTrail').onclick = function() {
        document.getElementById('timerStarter').disabled = true;
        document.getElementById('timerTrail').disabled = true;
        document.getElementById('timerTrail').setAttribute('class', 'timerb_on');
        
        startTrail(10000000000, []);
        timerRef = setInterval(startTrail, 60000, 10000000000, []);
        
        document.getElementById('timerTrail').value = 'Trailing On';
        document.getElementById('timerStopper').disabled = false;
    };
    
    document.getElementById('timerStopper').onclick = function() {
        document.getElementById('timerStopper').disabled = true;
        
        window.clearInterval(timerRef);
        
        document.getElementById('timerStarter').value = 'Voting System Off';
        document.getElementById('timerStarter').disabled = false;
        document.getElementById('timerStarter').setAttribute('class', 'timerb_off');
        
        document.getElementById('timerTrail').value = 'Trailing Off';
        document.getElementById('timerTrail').disabled = false;
        document.getElementById('timerTrail').setAttribute('class', 'timerb_off');
    };
    
    document.getElementById('AddBlacklist').onclick = function () {
        var blzoneRef = document.getElementById("BLzone");
        blzoneRef.insertAdjacentHTML('beforeend', '

'
); } steem.api.getNextScheduledHardfork(function(err, result) { if (!err) { document.getElementById('Hfv').value = result.hf_version; document.getElementById('Hft').value = result.live_time; } else { errorHandler(err); } }); }, false);

While slightly messy, the above shows exactly what each button does especially in relation to other buttons. With my setup, only one button and one voting system can be on at a time. I now use .setAttribute('class', 'any_class_name'); to set the button classes for CSS control.

startTrail(10000000000, []);
timerRef = setInterval(startTrail, 60000, 10000000000, []);

The code above in the onclick function for our new Trial button calls the function to begin our trail chain, and uses the same timerRef variable to reference the interval on which it will run. The 60000 is one minute for the interval, and the two arguments after are for the function.

Now that we have tweaked our files to make new additions to functionality easier, I will create a new file in our folder/directory called trail.js as the functionality will be stand-alone. You do not need to create a new file and instead write the new functions in the main.js, but to make things easy to read, I will add a new js file and load it into my html after steem.min.js but before the main.js:

<head>
        
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <script src="steem.min.js"></script>
    <script src="trails.js"></script>
    <script src="main.js"></script>
    <link rel="stylesheet" href="main.css">
        
</head>

Steem-JS options to get Account Votes.

With that all finished, the next part is looking into how to get the votes made by an account. The most obvious and surprisingly slowest option is:

steem.api.getAccountVotes('voter', function(err, result) {
  console.log(err, result);
});

This returns a list of every single vote ever cast by an account and sometimes takes a second or two to complete. We do not need to process over or store that much info every time we want to cast a vote, so we will look elsewhere.

What seems to be popular for trails is the stream functions, such as:

var opStream = steem.api.streamOperations(function(err, result) {
    console.log(err, result);
    // opStream(); // calling the variable name as a function ends the stream.
});

Using the stream function above lets you easily check if the operations constantly being made are votes and if they are made by the trail account. The problem is that most operations will not fit and the processing on each will continue non-stop which is not particularly necessary if the trail voter has already cast the vote.

Keeping all that in mind, I have decided to use:

steem.api.getAccountHistory(account, from, limit, function(err, result) {
  console.log(err, result);
});

By limiting how many indexes to search back per request, this function turns out to be fairly fast and efficient especially when called on an interval. Here is my startTrail function completed:

trail.js

function startTrail(startIndex, preQualList) {
    steem.api.getAccountHistory(
        document.getElementById('TAccount').value, // Account name, as a string.
        startIndex, // From where to begin getting older entries.
        50, // Amount of entries to get prior to most recent/whatever start index you use.
        function (err, result) {
            if (!err) {
                let nowDate = new Date,
                    opsDate;
                nowDate = Date.UTC(nowDate.getUTCFullYear(), nowDate.getUTCMonth(), nowDate.getUTCDate(), nowDate.getUTCHours(), nowDate.getUTCMinutes(), nowDate.getUTCSeconds(), nowDate.getUTCMilliseconds()) - (document.getElementById('TrailTime').value * 60000); // Minutes * milliseconds in a minute
                
                for (var i = result.length - 1; i >= 0; i--) {
                    opsDate = new Date(result[i][1].timestamp + 'Z'),
                        opsDate = opsDate.getTime();
                    
                    if (result[i][1].op[0] == 'vote' && result[i][1].op[1].voter == document.getElementById('TAccount').value && opsDate > nowDate) {
                        preQualList.push(String(result[i][1].op[1].author + '/' + result[i][1].op[1].permlink));
                    } else if (opsDate <= nowDate) {
                        totalIts++;
                        document.getElementById('TotalIts').value = totalIts;
                        voteQualifiers = preQualList;
                        voteIteration = Number(0);
                        checkBlacklist();
                        break;
                    }
                    if (i == 0 && opsDate > nowDate) {
                        startTrail(result[i][0] - 1, preQualList);
                    }
                }
            } else {
                errorHandler(err);
            }
        }
    );
}

The function begins with an argument of the start index defaulting at 10000000000 which is just most recent history, and an empty array which will be the temporary container for the posts we gather. Next we call the Steem-JS account history function and begin to process the information. As I have done in the feed function, I use dates to figure out how far back to obtain account history, and use a "reverse for loop" because the last index of the returned result is the most recent, and we want to start from the most recent operation. I made an input in our control panel to check how far back to obtain history, and this is where that "Trail Backtrack Time in Minutes" input comes in to create a date. If our interval is one minute, then I would normally use one minute for the date as well, so you would not need to check if you've already voted on that post. Essentially, the first part of the loop checks if the history entry is a vote operation made by the trail account, and if it is within our time limit. If not, it checks whether it is past the time and if so, moves on toward voting and breaks out of the loop. If the loop finishes and the entries were all within the time limit, it chains the startTrail function again feeding the post array and new start index. I also format the post as author/permlink like that obtained by our feed function to standardize the format.

Next I will rebuild the vote functionality so that the voteQualifiers variable can be filled by any functionality you want, and then the blacklist, voting power threshold, and vote will be standardized and reusable. I will do this in my trail.js without changing anything in the main.js and use the new functions only with the trail functionality. Here is the rest of the trail.js file.

trail.js

function checkBlacklist() {
    let blackListArr = Array.from(document.getElementsByClassName('Blacklist'));
    blackListArr = blackListArr.map(function (elem) {
        return elem.value;
    });
    for (var i = 0; i < voteQualifiers.length; i++) {
        if (blackListArr.indexOf((voteQualifiers[i].split('/'))[0]) != -1) {
            voteQualifiers.splice(i, 1);
        }
    }
    checkVpPerVote(0);
}

function checkVpPerVote(errBreak) {
    steem.api.getAccounts(['guest123'], function(err, result) {
        document.getElementById('TrueVPower').value = err ? 'getAccounts API error' : vpFormater(result[0].voting_power, result[0].last_vote_time);
        document.getElementById('VPower').value = err ? 'getAccounts API error' : result[0].voting_power;
        if (!err) {
            if (vpFormater(result[0].voting_power, result[0].last_vote_time) > document.getElementById('VPT').value) {
                document.getElementById('Log').innerHTML = JSON.stringify(result);
                voteTrail(errBreak);
            } else {
                document.getElementById('Log').innerHTML = 'Voting Power Below Threshold';
            }
        } else {
            errorHandler(err);
        }
    });
}

function voteTrail(errBreak) {
    if (voteIteration != voteQualifiers.length && voteQualifiers.length != 0) {
        let splitterRef = voteQualifiers[voteIteration].split('/');
        steem.broadcast.vote('5JRaypasxMx1L97ZUX7YuC5Psb5EAbF821kkAGtBj7xCJFQcbLg', 'guest123', splitterRef[0], splitterRef[1], 1000, function (err, result) {
            if (!err) {
                voteIteration++;
                totalVotes++;
                document.getElementById('TotalVotes').value = totalVotes;
                document.getElementById('Log').innerHTML = JSON.stringify(result);
                setTimeout(checkVpPerVote, 3200, 0);
            } else {
                errorHandler(err);
                errBreak++;
                if (errBreak >= 2) {
                    voteIteration++;
                    errBreak = 0;
                }
                setTimeout(checkVpPerVote, 3200, errBreak);
            }
        });
    } else {
        document.getElementById('Log').innerHTML = 'No posts to vote right now.';
    }
}

The first function is our blacklist check which now plucks out posts based on the author with .splice(i, 1) with i being the index and 1 being how many entries to delete. (voteQualifiers[i].split('/'))[0] returns the author and while the parentheses around voteQualifiers[i].split('/') before the [0] are unnecessary, they simply specify directly that the .split('/') should finish and return an array that the [0] will act on. This checkBlacklist() would be the function to call after you complete the voteQualifiers array with posts however you choose to do that. After that, I simply reuse the vp check function and chain it together with the actual vote function to maintain our errorBreak and check against the vp threshold on every vote.

I hope that this tutorial was informative and helpful, and that you enjoyed. By setting the backtrack time in the control panel to the same time as your interval, this should be able to avoid missing votes and trying to revote on already voted posts. You could check each post to make sure you have not voted on it, but this tutorial was designed to avoid as much API calling as possible to retain control and increase performance while maybe illustrating another way to trail vote. Thanks for reading and as always, have a good one!



Posted on Utopian.io - Rewarding Open Source Contributors

Steem-JS Following Feed Voting Bot, featuring html GUI and dynamic ... | Ecency