├── Examples ├── example1.yaml └── example2.yaml ├── README.md └── autoupvote-bot.py /Examples/example1.yaml: -------------------------------------------------------------------------------- 1 | # change the settings for your preferences and setup 2 | settings : 3 | wallet_password : walletpword 4 | rpc_ip : "127.0.0.1" 5 | rpc_port : 8091 6 | rpc_user : "rpcuser" 7 | rpc_password : "rpcpassword" 8 | log_file : "autoupvote.log" 9 | debug : true 10 | monitor : 11 | complexring : 12 | your_account_name : 13 | min_random_wait : 100 14 | max_random_wait : 800 15 | frequency : 1 16 | 17 | -------------------------------------------------------------------------------- /Examples/example2.yaml: -------------------------------------------------------------------------------- 1 | # change the settings for your preferences and setup 2 | settings : 3 | wallet_password : walletpword 4 | rpc_ip : "127.0.0.1" 5 | rpc_port : 8091 6 | rpc_user : "rpcuser" 7 | rpc_password : "rpcpassword" 8 | log_file : "autoupvote.log" 9 | debug : true 10 | monitor : 11 | account_to_monitor_1 : 12 | account_to_vote_with_1_1 : 13 | min_random_wait : 0 14 | frequency : 1 15 | account_to_vote_with_1_2 : 16 | min_random_wait : 60 # in seconds 17 | max_random_wait : 1200 18 | frequency : .75 # probability that vote occurs 19 | account_to_monitor_2 : 20 | account_to_vote_with_2_1 : 21 | min_random_wait : 300 # in seconds 22 | max_random_wait : 900 23 | frequency : .5 # probability that vote occurs 24 | account_to_monitor_3 : 25 | account_to_vote_with_3_1 : 26 | min_random_wait : 3600 # in seconds 27 | max_random_wait : 7200 28 | frequency : .3 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # STEEM Autovote Bot 2 | 3 | Introduction 4 | ===================== 5 | 6 | [STEEM Autovote](https://github.com/matthewniemerg/python-steem-autovote) is a simple autovoting solution for [STEEM](https://steemit.com/) users. Features include : 7 | 8 | * Simple, customizable [YAML](http://www.yaml.org) configuration file. 9 | * Allows users to monitor multiple accounts and autovote posts (not comments) 10 | * Multiple accounts can autovote any monitored account 11 | * Monitored accounts can be autovoted immediately or within a random time frame for each voting account 12 | * Monitored accounts can be autovoted with a prescribed frequency for each voting account 13 | * Upvotes only 14 | 15 | Dependencies 16 | ===================== 17 | 18 | STEEM Autovote has only two dependencies: [PyYaml](http://pyyaml.org/) and 19 | [Requests](http://docs.python-requests.org/). 20 | On [Ubuntu](http://www.ubuntu.com/), these dependencies 21 | can be installed with the following command: 22 | 23 | ``` 24 | sudo apt-get install python-yaml python-requests 25 | ``` 26 | 27 | Depending on your system's python configuration, additional python packages may need to be installed. 28 | First, we will need to install python's pip. 29 | 30 | ``` 31 | sudo easy_install pip 32 | ``` 33 | 34 | Once pip is installed, we can install the other packages. This is accomplished with the following command: 35 | 36 | ``` 37 | sudo pip install python-dateutil 38 | ``` 39 | 40 | 41 | Configuration 42 | ===================== 43 | 44 | Example configuration files are provided in the `Examples` directory as `example1.yaml` and `example2.yaml`. 45 | The configuration file is specified when calling the `autovote-bot.py` script: 46 | 47 | ``` 48 | python autovote-bot.py /home/username/autovote/autovote.yaml 49 | ``` 50 | 51 | Editting the yaml file may be difficult at first, but the rules are quite easy to remember. 52 | 53 | * Do not allow for tabbed spaces, only single character white spaces and hard carriage returns. 54 | * New accounts to monitor are added in the monitor section. 55 | * Allow for two additional white spaces for each sub-list. 56 | * The outermost list is the account to monitor. 57 | * Voting ccounts of a monitored account are items in the sub-list of a monitored account. 58 | * Two entries are required in the sub-list for each voting account: random_wait and frequency. 59 | 60 | 61 | Example Configuration File 62 | ====================== 63 | 64 | ``` 65 | settings : 66 | wallet_password : walletpword 67 | rpc_ip : "127.0.0.1" 68 | rpc_port : 8091 69 | rpc_user : "rpcuser" 70 | rpc_password : "rpcpassword" 71 | log_file : "autoupvote.log" 72 | debug : true 73 | monitor : 74 | complexring : 75 | your_account_name : 76 | random_wait : 0 # random wait time, anytime from 0 to 0 seconds 77 | frequency : 1 # probability that a vote will occur 78 | your_sock_puppet_account : 79 | random_wait : 100 # random wait time, anytime from 0 to 100 seconds 80 | frequency : .1 # probability that a vote will occur 81 | your_account_name : 82 | your_account_name : 83 | random_wait : 0 # random wait time, anytime from 0 to 0 seconds 84 | frequency : 1 # probability that a vote will occur 85 | your_sock_puppet_account : 86 | random_wait : 60 # random wait time, anytime from 0 to 60 seconds 87 | frequency : .5 # probability that a vote will occur 88 | your_sock_puppet_account : 89 | your_account_name : 90 | random_wait : 1200 # random wait time, anytime from 0 to 1200 seconds 91 | frequency : .333 # probability that a vote will occur 92 | your_sock_puppet_account : 93 | random_wait : 0 # random wait time, anytime from 0 to 0 seconds 94 | frequency : 1 # probability that a vote will occur 95 | ``` 96 | 97 | Random Settings 98 | =================== 99 | 100 | The block_id hash of the post of a monitored account is used as a seed in python's Mersennes Twister Pseudo Random Number Generator. This seed gets updated for each new post of any monitored account. 101 | 102 | 103 | Running STEEM Autovote Bot 104 | =================== 105 | 106 | Running the autovote-bot script requires an open wallet, an instance of `cli_wallet` must be run as a daemon process, listening on an RPC port. On Ubuntu, 107 | this is best achieved using [Upstart](http://upstart.ubuntu.com/) services. 108 | 109 | Please see [this guide](https://github.com/steemed/steem-price-feed/) for starting an upstart service for your cli_wallet. 110 | 111 | Alternatively, you can run `cli_wallet` in an instance of a screen. 112 | 113 | After installing `screen` type 114 | 115 | `screen` 116 | 117 | and then once you return to the shell, navigate to the cli_wallet directory and then type 118 | 119 | ```./cli_wallet -u user -p password --rpc-endpoint=127.0.0.1:8091 -d 2>cli-debug.log 1>cli-error.log``` 120 | 121 | Detach the screen with `Ctrl + a` and then `Ctrl + x` and you now have a `cli_wallet` daemon running. 122 | 123 | There are at least 2 ways you can run the STEEM Autovote Bot. 124 | 125 | * Use `screen` and navigate to the appropriate directory, and then run this process in the screened shell with `python autovote-bot.py autovote.yaml` 126 | * Use an upstart service 127 | 128 | 129 | 130 | Running as an Upstart Service 131 | =================== 132 | 133 | It is highly desirable to run the STEEM Autovote Bot as an upstart service so that on reboot and termination, a respawn of the process will occur. 134 | 135 | Save the following script in `/etc/init/steem-autovote-bot.conf` (editted for your own system) 136 | 137 | ``` 138 | # steem-autovote-bot service - steem-autovote-bot service for user 139 | 140 | description "STEEM Autovote bot" 141 | author "Ima User " 142 | 143 | # Stanzas 144 | # 145 | # Stanzas control when and how a process is started and stopped 146 | # See a list of stanzas here: //upstart.ubuntu.com/wiki/Stanzas 147 | 148 | # When to start the service 149 | start on runlevel [2345] 150 | 151 | # When to stop the service 152 | stop on runlevel [016] 153 | 154 | # Automatically restart process if crashed 155 | respawn 156 | 157 | # Essentially lets upstart know the process will detach itself to the background 158 | # This option does not seem to be of great importance, so it does not need to be set. 159 | # expect fork 160 | 161 | # Specify working directory 162 | chdir /home/user/path/to/steem-autovote 163 | 164 | # Specify the process/command to start, e.g. 165 | exec /usr/bin/python autovote-bot.py autovote.yaml 2>autovote-debug.log 1>autovote-error.log 166 | ``` 167 | 168 | Upcoming Features 169 | ================== 170 | 171 | * Upvote with weights 172 | * Downvote with weights 173 | * Random Interval (not just from 0 to random_wait) 174 | * Tracking of when (auto)votes occurred and adjusting times to vote to maximize voting power for both immediate votes and queued votes 175 | 176 | Acknowledgments 177 | =================== 178 | 179 | I have heavily modified the STEEM witness [steemed's](https://steemit.com/witness-category/@steemed/steemed-witness-thread) source code for creating a [STEEM Price Feed](https://github.com/steemed/steem-price-feed/). -------------------------------------------------------------------------------- /autoupvote-bot.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | from time import gmtime, strftime 3 | import os 4 | import sys 5 | import time 6 | import math 7 | import json 8 | import random 9 | import signal 10 | import logging 11 | import datetime 12 | import bisect 13 | import dateutil.parser 14 | 15 | import requests 16 | import yaml 17 | from operator import itemgetter, attrgetter, methodcaller 18 | 19 | SQRT2 = math.sqrt(2.0) 20 | 21 | # no real reason to make any of these configurable 22 | SEC_PER_HR = 3600.0 # 3600 seconds/hr 23 | MAX_HIST = 1000 # 1000 tx 24 | SLEEP_GRANULARITY = 0.25 # 0.25 sec 25 | LOOP_GRANULARITY = 0.25 # 1/4 of the min_publish_interval 26 | 27 | 28 | 29 | 30 | 31 | class DebugException(Exception): 32 | pass 33 | 34 | class GracefulKiller: 35 | # https://stackoverflow.com/a/31464349 36 | kill_now = False 37 | def __init__(self): 38 | signal.signal(signal.SIGINT, self.exit_gracefully) 39 | signal.signal(signal.SIGTERM, self.exit_gracefully) 40 | 41 | def exit_gracefully(self,signum, frame): 42 | self.kill_now = True 43 | 44 | 45 | class WalletRPC(object): 46 | def __init__(self, ip, port, rpcuser, rpcpassword): 47 | self.url = "http://%s:%s/rpc" % (ip, port) 48 | self.rpcuser = rpcuser 49 | self.rpcpassword = rpcpassword 50 | self._headers = {'content-type': 'application/json'} 51 | self._jsonrpc = "1.0" 52 | self._id = 1 53 | self._auth = (rpcuser, rpcpassword) 54 | def __call__(self, method, params=None): 55 | if params is None: 56 | params = [] 57 | else: 58 | params = list(params) 59 | payload = { 60 | "method": method, 61 | "params": params, 62 | "jsonrpc": self._jsonrpc, 63 | "id": self._id 64 | } 65 | data = json.dumps(payload) 66 | response = requests.post(self.url, data=data, 67 | headers=self._headers, auth=self._auth) 68 | return response.json() 69 | def is_locked(self): 70 | return self("is_locked") 71 | def unlock(self, password): 72 | if self.is_locked(): 73 | return self("unlock", [password]) 74 | return True 75 | 76 | 77 | def vote(self, voter, author, permlink, weight, broadcast): 78 | return self("vote", [voter, author, permlink, weight, broadcast]) 79 | 80 | 81 | def get_account(self, accountname): 82 | return self("get_account", [accountname]) 83 | 84 | 85 | def info(self): 86 | return self("info") 87 | def get_block(self, block_num): 88 | return self("get_block", [block_num]) 89 | 90 | def get_state(self, state): 91 | return self("get_state", [state]) 92 | 93 | def timestamp(dt): 94 | delta = dt - datetime.datetime(1970, 1, 1) 95 | return delta.total_seconds() 96 | 97 | 98 | 99 | def usage(message=None): 100 | if message is not None: 101 | print "##" 102 | print "## ERROR: %s" % message 103 | print "##" 104 | print 105 | 106 | print "usage: %s " % os.path.basename(sys.argv[0]) 107 | raise SystemExit 108 | 109 | 110 | 111 | 112 | 113 | def load_config(config_name): 114 | with open(config_name) as f: 115 | s = f.read() 116 | try: 117 | config = yaml.safe_load(s) 118 | except Exception, e: 119 | usage(str(e)) 120 | return config 121 | 122 | 123 | def access(r, accessor): 124 | for i in accessor: 125 | try: 126 | r = r[i] 127 | except: 128 | raise TypeError("Can not access attribute '%s'." % (i,)) 129 | return r 130 | 131 | def process_block(wallet,settings, last_block, voting_queue): 132 | block_tx_info = wallet.get_block(last_block)["result"]["transactions"] 133 | # print "block_tx_info =", block_tx_info 134 | for a in block_tx_info: 135 | if "operations" in a: 136 | random.seed(str(wallet.get_block(last_block)["result"]["block_id"])) 137 | 138 | for cur_oper in a["operations"]: 139 | if cur_oper[0] == "comment": 140 | if cur_oper[1]["author"] in settings["monitor"]: 141 | if cur_oper[1]["parent_author"] == "": #this means this is an original post and not a comment 142 | monitored_account = cur_oper[1]["author"] 143 | # go through all of the controlled accounts to upvote with and determine if and when 144 | for b in settings["monitor"][monitored_account]: 145 | randval = random.random() 146 | vote_command = [b, monitored_account, cur_oper[1]["permlink"], 100, True] 147 | if 1-settings["monitor"][monitored_account][b]["frequency"] < randval: 148 | min_wait = settings["monitor"][monitored_account][b]["min_random_wait"] 149 | max_wait = min_wait 150 | if 'max_random_wait' in settings["monitor"][monitored_account][b].keys(): 151 | max_wait = settings["monitor"][monitored_account][b]["max_random_wait"] 152 | wait_in_seconds = random.random()*(max_wait-min_wait) + min_wait 153 | state_to_get = cur_oper[1]["parent_permlink"]+"/@"+cur_oper[1]["author"]+"/"+cur_oper[1]["permlink"] 154 | my_state = wallet.get_state(state_to_get)["result"] 155 | a_new_key = cur_oper[1]["author"]+"/"+cur_oper[1]["permlink"] 156 | some_more_info = my_state["content"][a_new_key] 157 | 158 | if some_more_info["last_update"] == some_more_info["created"]: 159 | if wait_in_seconds > 0: 160 | time_to_add = wait_in_seconds+time.time() 161 | add_to_queue = [time_to_add, vote_command] 162 | voting_queue.insert(bisect.bisect_left(voting_queue, add_to_queue, 0, len(voting_queue)), add_to_queue) 163 | print "will broadcast vote(", b, monitored_account, cur_oper[1]["permlink"], 100, "True)", "in", wait_in_seconds, "seconds" 164 | else: 165 | print "Autovote occured with ", vote_command 166 | myresponse = wallet.vote(vote_command[0], vote_command[1], vote_command[2], vote_command[3], vote_command[4]) 167 | #print myresponse 168 | else: 169 | print "Bot scanned editted post, not upvoting." 170 | else: 171 | print "Probability of voting event occuring failed. Did not add vote(", vote_command, ")." 172 | 173 | 174 | 175 | 176 | 177 | 178 | return False 179 | 180 | def monitor_loop(settings, wallet): 181 | killer = GracefulKiller() 182 | debug = settings.get("debug", False) 183 | # secret setting for devs, disable if you don't want to publish 184 | is_live = settings.get("is_live", True) 185 | 186 | logfile_name = settings.get("log_file", None) 187 | 188 | if debug: 189 | log_level = logging.DEBUG 190 | else: 191 | log_level = logging.INFO 192 | 193 | # secret advanced user setting, see https://docs.python.org/2/howto/logging.html 194 | log_format = settings.get("log_format", "%(levelname)s: %(message)s") 195 | if logfile_name is None: 196 | logging.basicConfig(format=log_format, level=log_level) 197 | else: 198 | logging.basicConfig(format=log_format, filename=logfile_name, level=log_level) 199 | cur_info = wallet.info() 200 | last_block = cur_info["result"]["last_irreversible_block_num"] 201 | 202 | # last_block = 203 | 204 | 205 | 206 | 207 | voting_queue = [] 208 | process_block(wallet,settings, last_block, voting_queue) 209 | 210 | 211 | blocks_processed = 1 212 | while True: 213 | if logfile_name is None: 214 | logfile = sys.stdout 215 | else: 216 | logfile = open(logfile_name, "a") 217 | loop_time = time.time() 218 | 219 | min_pub_intrvl = 10 220 | do_update = False 221 | # test if a new block has been found 222 | 223 | current_time = time.time() 224 | current_block = cur_info["result"]["last_irreversible_block_num"] 225 | 226 | if current_block > last_block: 227 | last_block = last_block+1 228 | blocks_processed = blocks_processed+1 229 | process_block(wallet,settings,last_block,voting_queue) 230 | 231 | if blocks_processed % 100 == 0: 232 | print "blocks_processed = ", blocks_processed, "last_block = ", last_block 233 | if (len(voting_queue)) != 0: 234 | print "There are", len(voting_queue), "votes in the queue." 235 | print "Next vote broadcast in", time.time() - voting_queue[0][0], "seconds" 236 | 237 | if len(voting_queue) != 0: 238 | keeppopping = True 239 | if current_time < voting_queue[0][0]: 240 | keeppopping = False 241 | while keeppopping: 242 | if current_time > voting_queue[0][0]: 243 | vote_command = voting_queue.pop(0) 244 | time_to_vote = vote_command[0] 245 | 246 | # now go ahead and vote 247 | wallet.vote(vote_command[1][0], vote_command[1][1], vote_command[1][2], vote_command[1][3], vote_command[1][4]) 248 | print "Voting with vote(", vote_command[1], ")" 249 | print "length of voting queue =", len(voting_queue) 250 | 251 | if len(voting_queue) == 0: 252 | keeppopping = False 253 | else : 254 | print "next vote broadcast will occur in", time.time() - voting_queue[0][0], "-- negative time indicates continued broadcasting of votes in queue" 255 | else : 256 | keeppopping = False 257 | if len(voting_queue) != 0: 258 | print "waiting to vote for :", time.time() - voting_queue[0][0], "seconds" 259 | 260 | cur_info = wallet.info() 261 | while (time.time() - loop_time) < (min_pub_intrvl * LOOP_GRANULARITY): 262 | if killer.kill_now: 263 | logging.info("Caught kill signal, exiting.") 264 | break 265 | time.sleep(SLEEP_GRANULARITY) 266 | if killer.kill_now: 267 | break 268 | cur_info = wallet.info() 269 | 270 | 271 | 272 | def main(): 273 | if len(sys.argv) != 2: 274 | usage() 275 | config_name = sys.argv[1] 276 | if not os.path.exists(config_name): 277 | usage('Config file "%s" does not exist.' % config_name) 278 | if not os.path.isfile(config_name): 279 | usage('"%s" is not a file.' % config_name) 280 | config = load_config(config_name) 281 | 282 | settings = config['settings'] 283 | wallet = WalletRPC(settings['rpc_ip'], settings['rpc_port'], 284 | settings['rpc_user'], settings['rpc_password']) 285 | 286 | if not wallet.unlock(settings['wallet_password']): 287 | print("Can't unlock wallet with password. Aborting.") 288 | raise SystemExit 289 | 290 | monitor = settings['monitor'] 291 | 292 | print monitor 293 | for a in monitor: 294 | for b in monitor[a]: 295 | my_max_random_wait = monitor[a][b]['min_random_wait'] 296 | if 'max_random_wait' in monitor[a][b].keys(): 297 | my_max_random_wait = monitor[a][b]['max_random_wait'] 298 | print b, " is monitoring", a, "with a random wait between", monitor[a][b]['min_random_wait']," and" , my_max_random_wait, "seconds with a probability of", monitor[a][b]['frequency'] 299 | 300 | 301 | monitor_loop(settings, wallet) 302 | 303 | 304 | 305 | if __name__ == "__main__": 306 | main() 307 | 308 | --------------------------------------------------------------------------------