HOWTO Build a Dittobot!

in #steemit4 years ago

ditto.png
Ok first thing's first. What's a dittobot and why would you want one?

A dittobot is bot that watches a watchlist of steemians and when they cast a vote, it votes in kind.
This is a useful way to learn the internals of steem while supporting people who's views, goals, or opinions are much like your own.

So let's begin...

I am using ubuntu 18.04 with node 12. The process should be the same on OSX, but I don't know if Windows will work for this. There's no reason it shouldn't, but I haven't used Windows since XP, so I'd have no idea what to do differently.

The first step is to create a nodejs project (actual first step is to install nodejs and npm, but I'll leave that as an exercise for the reader).

You can create a new node project with npm init , just accept the defaults for init unless you have reason not to.

username@computer:~/Projects$ mkdir ditto-bot
username@computer:~/Projects$ cd ditto-bot
username@computer:~/Projects/ditto-bot$ npm init
username@computer:~/Projects/ditto-bot$ touch index.js

At this point you should have a directory containing 2 files, package.json and index.js

We are going to need dsteem to begin with and eventually steemjs. Beyond that we will also need event-stream and util. It's a good idea to get all of those installed now.

username@computer:~/Projects/ditto-bot$ npm install -s dsteem
username@computer:~/Projects/ditto-bot$ npm install -s steem
username@computer:~/Projects/ditto-bot$ npm install -s event-stream
username@computer:~/Projects/ditto-bot$ npm install -s util

You're going to see a lot of messages fly by and that's ok. If you don't get any errors you should now open index.js in your favorite editor.

It will be empty.

You should instantiate both dsteem and steemjs as follows.

const steem = require("steem");
const {Client} = require("dsteem");

let es = require("event-stream"); 
let util = require("util");

const client = new Client('https://api.steemit.com')

steem.api.setOptions({ url: 'https://api.steemit.com' });

I know this is sort of a kitchen sink approach. But that's everything you need to bring in from the outside world.

The next step is to think through what you want your dittobot to do.
In my case I have 2 users accounts I manage and work with. I want them to each upvote from a watchlist I create. For now we're going to have the watchlist be one another. That way when either of these accounts votes, the other one will as well.

You can also look at adding some curators from the outside world. If you look at my previous post that should give you some ideas, such as @daan and @teamsteem but you probably have your own favs.

The watchlist is just a simple array of names to watch out for.

const watchlist = ['benicents','benitrade'];

Notice there is no @ , the @ sign is not really used by the code. So you always want to put in @username as simply username, whenever you update your watchlist.

The next step is to setup the user(s) you will be voting for.
This requires your posting key, which is probably what you're already using for steem.

For a toy like this it's fine just to hardcode the user list.

var users = [
    {
        name: "benicents",
        key : "5K..."
    },
    {
        name: "benitrade",
        key : "5K..."
    }
];

Just don't upload the code like that to a public repository like github. If you're going to upload the code. Put the userlist in a json file and then just "require" it.

const users = require('./users.json');

Then make sure that users.json file is in your .gitignore.

Ideally you should use steemconnect or a database for this info, but that's outside the scope of this tutorial.

We will need to check our voting power to make sure we don't run out, and while we're at it let's make sure we don't leave any unclaimed funds sitting around, which is easy to do at the same time we're checking out voting power.

async function checkPower(){
    users.forEach(async (user,idx)=>{
        let result = await steem.api.getAccountsAsync([user.name]);
        result = result[0];
        //console.debug("Result: ",result);
        users[idx].power = result.voting_power;
        console.debug(`${user.name} has ${(users[idx].power / 100).toFixed(2)}% Power Remaining`);
        if(result.reward_sbd_balance != "0.000 SBD" || result.reward_steem_balance != "0.000 STEEM" || result.reward_vesting_balance != "0.000000 VESTS"){
            console.debug(`${user.name} has ${result.reward_sbd_balance} , ${result.reward_steem_balance} & ${result.reward_vesting_balance} SP unclaimed, claiming it now`);
            result = steem.broadcast.claimRewardBalanceAsync(users[idx].key,user.name,result.reward_steem_balance,result.reward_sbd_balance,result.reward_vesting_balance);
        }else{
            console.debug(`${user.name} has no unclaimed funds`);
        }
    });
}

The next step is to connect the firehose!

The firehose is the livestream of all activity going on in steemit.
We use dsteem for this and looks like...

async function openFireHose(){
    let stream = client.blockchain.getBlockStream();
    stream.pipe(es.map(function(block, callback) {
        //console.debug("block: ",block);
        block.transactions.forEach((tx)=>{
            //console.debug("tx: ",tx);
            for(ops of tx.operations){
                for(op of ops){
                    if(op.voter && watchlist.indexOf(op.voter) != -1){
                        console.debug("vote: ",op);
                        for(user of users){
                            handleVote(user,op);
                        }
                        
                        callback(null, util.inspect(op, {colors: true, depth: null}) + '\n');
                    }else{
                        console.debug(op.voter+" not found in watchlist");
                    }
                }
            }
        });
    })).pipe(process.stdout);
} 

That sure is a lot of code, but here's what it does.
First we open a stream using dsteem and connect it to the blockstream.
Now every single block that is mined will come into our pipe.

Inside the pipe we are looping through every tx in the block.
Then for each tx we look at all the ops or operations.

If the op happens to be a vote we look at the voter.
If that voter is in our watchlist we pass it to the handleVote function once for each user we are voting with.

Now is a good time to mention, that if you aren't exclusively following your own accounts, you should take into consideration how much voting power is remaining on your account. That is outside the scope of this tutorial though since the intent is that you follow only your own accounts and perhaps one or two friends. 8000 corresponds to 80% and we don't recommend you vote if your power is down that low.

The handleVote function is dead simple.

async function handleVote(user,info){
    if(user.power <= 8000){
        console.debug(`${user.name} is unable to vote, only ${(user.power / 100).toFixed(2)}% voting power remains.`);
        return;
    }else{
        console.debug(`${user.name} has ${(user.power / 100).toFixed(2)}% voting power, they can vote!`);    
    }

   steem.broadcast.vote(user.key, user.name, info.author, info.permlink, Math.abs(info.weight), 
   (err, result)=>{
    //console.debug(`Vote Result for : ${user.name}`);
    if(err){
        console.error(err);
    }else{
        console.log(`Vote by ${user.name} accepted at block ${result.block_num}`);
    }
  });
}![ditto.png](https://cdn.steemitimages.com/DQmbXkJbYThEHGE8KkLe6sjbxUQJvKwxFXLx8xnw9xaRJR9/ditto.png)

As you can see, all we've done is copy the author, permlink and weight. However we perform Math.abs to the weight.
This corrects any weight to positive. Negative weight is downvoting and we don't want to accidentally downvote someone. If you are confident the user you are dittoing won't downvote without good reason (even accidently), you can just replace that with info.weight.

The final code should look like this...

const steem = require("steem");
const {Client} = require('dsteem');

var es = require('event-stream');
var util = require('util');

const client = new Client('https://api.steemit.com')

steem.api.setOptions({ url: 'https://api.steemit.com' });

const watchlist = ['benicents','benitrade'];

var users = require('./users.json');

async function checkPower(){
    users.forEach(async (user,idx)=>{
        let result = await steem.api.getAccountsAsync([user.name]);
        result = result[0];
        //console.debug("Result: ",result);
        users[idx].power = result.voting_power;
        console.debug(`${user.name} has ${(users[idx].power / 100).toFixed(2)}% Power Remaining`);
        if(result.reward_sbd_balance != "0.000 SBD" || result.reward_steem_balance != "0.000 STEEM" || result.reward_vesting_balance != "0.000000 VESTS"){
            console.debug(`${user.name} has ${result.reward_sbd_balance} , ${result.reward_steem_balance} & ${result.reward_vesting_balance} SP unclaimed, claiming it now`);
            result = steem.broadcast.claimRewardBalanceAsync(users[idx].key,user.name,result.reward_steem_balance,result.reward_sbd_balance,result.reward_vesting_balance);
        }else{
            console.debug(`${user.name} has no unclaimed funds`);
        }
    });
}

async function openFireHose(){
    let stream = client.blockchain.getBlockStream();
    stream.pipe(es.map(function(block, callback) {
        //console.debug("block: ",block);
        block.transactions.forEach((tx)=>{
            //console.debug("tx: ",tx);
            for(ops of tx.operations){
                for(op of ops){
                    if(op.voter && watchlist.indexOf(op.voter) != -1){
                        console.debug("vote: ",op);
                        for(user of users){
                            handleVote(user,op);
                        }
                        callback(null, util.inspect(op, {colors: true, depth: null}) + '\n');
                    }else{
                        //console.debug(op.voter+" not found in watchlist");
                    }
                }
            }
        });
    })).pipe(process.stdout);
}

async function handleVote(user,info){
    if(user.power <= 8000){
        console.debug(`${user.name} is unable to vote, only ${(user.power / 100).toFixed(2)}% voting power remains.`);
        return;
    }else{
        console.debug(`${user.name} has ${(user.power / 100).toFixed(2)}% voting power, they can vote!`);    
    }

   steem.broadcast.vote(user.key, user.name, info.author, info.permlink, Math.abs(info.weight), 
   (err, result)=>{
    //console.debug(`Vote Result for : ${user.name}`);
    if(err){
        console.error(err);
    }else{
        console.log(`Vote by ${user.name} accepted at block ${result.block_num}`);
    }
  });
}

checkPower();
openFireHose();

setInterval(()=>{
    console.debug("Checking remaining voting power!");
    checkPower();},1000 * 60 * 2);

And that's it. That's your ditto-bot!

Now you can just start it with "node index.js" and it will run until you tell it to stop with ctrl+c

If you want it to run forever, you can

sudo npm install -g forever

Forever is an app that will run your bot, watch if it dies and restart it automatically.
You can add it to your rc.local file if you want it to start when the computer starts.

forever /home/username/Projects/ditto-bot/index.js

That's everything for now! Enjoy! And remember as always this post 100% Steem Powered Up!

Sort:  

Congratulations @benicents! You have completed the following achievement on the Steem blockchain and have been rewarded with new badge(s) :

You distributed more than 50 upvotes. Your next target is to reach 100 upvotes.

You can view your badges on your Steem Board and compare to others on the Steem Ranking
If you no longer want to receive notifications, reply to this comment with the word STOP

To support your work, I also upvoted your post!

Vote for @Steemitboard as a witness to get one more award and increased upvotes!