Dr. Phil (drphil.rb
) is reimplementation of the "Winfrey" voting bot specification. The goal is to give everyone an upvote. See: https://steemit.com/radiator/@inertia/drphil-rb-voting-bot-update-fixes
This is the Dr. Phil bot for STEEM and GOLOS. Please have a look at the README.md file.
drphil.log
.rvmrc
favorite_accounts.txt │·
flag_signals.txt │·
skip_accounts.txt │·
skip_tags.txt │·
vote_signals.txt │·
voters.txt
source 'https://rubygems.org'
gem 'radiator'
GEM
remote: https://rubygems.org/
specs:
awesome_print (1.8.0)
bitcoin-ruby (0.0.17)
connection_pool (2.2.1)
ffi (1.9.21)
hashie (3.5.7)
json (2.1.0)
little-plugger (1.1.4)
logging (2.2.2)
little-plugger (~> 1.1)
multi_json (~> 1.10)
multi_json (1.13.1)
net-http-persistent (3.0.0)
connection_pool (~> 2.2)
radiator (0.3.15)
awesome_print (~> 1.7, >= 1.7.0)
bitcoin-ruby (~> 0.0, >= 0.0.11)
ffi (~> 1.9, >= 1.9.18)
hashie (~> 3.5, >= 3.5.5)
json (~> 2.0, >= 2.0.2)
logging (~> 2.2, >= 2.2.0)
net-http-persistent (>= 2.5.2)
PLATFORMS
ruby
DEPENDENCIES
radiator
BUNDLED WITH
1.16.1
Before you can use drphil
, you will need to install bundler
. If you are using the system version of ruby
, you need to use sudo
to install bundler
:
$ sudo gem install bundler
On modern versions of macOS, attempting to use git
will prompt its installation, which is a neat feature. Just type git
in the terminal and hit enter. You'll get this dialog:
Then you can proceed to do the normal installation steps.
$ git clone https://gist.github.com/61bcc2b821aa5acb24f7fc88921950c7.git drphil
$ cd drphil
$ bundle install
Then run it:
$ ruby drphil.rb
To configure drphil
, you will need to modify the drphil.yml
. You can edit it by typing open -e drphil.yml
in the terminal.
For these steps, we will install the cygwin
package manager, which will provide the support packages and dependencies we need.
We need cygwin
to install git
, ruby-dev
, gem
, and make
for us. Run its setup and take all of the defaults, clicking Next
until you reach the Select Packages
dialog. Select the View
option of Full
. Search for:
git
and find the package named: git: Distributed version control
. Change Skip
to 2.12.2-1
(or later).ruby-dev
and find the package named: ruby-devel: Interpreted object-oriented scripting language
. Change Skip
to 2.3.3-1
(or later).gem
and find the package named: rubygem: Ruby module management
. Change Skip
to 2.3.3-1
(or later).make
and find the package named: make: The GNU version of the 'make' utility
. Change Skip
to 4.2.1-1
(or later).Click Next
until cygwin
is done installing these packages.
Open the Cygwin Terminal
Now, we can continue with the usual install:
$ git clone https://gist.github.com/61bcc2b821aa5acb24f7fc88921950c7.git drphil
$ cd drphil
$ bundle install
Then run it:
$ ruby drphil.rb
To configure drphil
, you will need to modify the drphil.yml
which will be located in:
C:\cygwin\home\<username>\drphil
If you're using Windows XP, official cygwin
support has been dropped. There is an unofficial project here:
http://www.crouchingtigerhiddenfruitbat.org/Cygwin/timemachine.html
unique_author
logic to use account_history
instead of internal timer for improved accuracy between runs.voting_rules
winfrey
mode that acts like the winfrey bot, all voters vote for everyonedrphil
mode one random voter votes for everyone (default)following_vote_weight
- for accounts that the voter followsfollowers_vote_weight
- for accounts that follow the votermin_rep
(default 25.0
)min_wait
and max_wait
(in minutes) so that you can fine-tune voting delay.favorite_accounts
list and separate favorites_vote_weight
option.
enable_comments
option to vote for post replies (default false).only_first_posts
option to only vote on an author's first post (default false).max_rep
option, useful for limiting votes to newer authors (default 99.9).vote_signals
account list.
drphil
mode).min_rep
can now accept either a static reputation or a dynamic property.
25.0
dynamic:100
. This will occasionally query the top 100 trending posts and use the minimum author reputation.vote_weight: 0.00 %
and skipping without broadcast.
min_voting_power
to create a floor with will allow the voter to recharge over time without having to stop the script.voters
as a separate filename. E.g:
voters: voters.txt
account wif
(no leading dash, separated by space)favorite_accounts
skip_accounts
skip_tags
flag_signals
vote_signals
only_fully_powered_up
which will only vote for posts that receive 100% STEEM Power author rewards.replay:
allows a replay of n blocks allowing you to catch up to the present.
ruby drphil.rb replay:90
will replay the last 90 blocks (about 4.5 minutes).^C
will have.cashout_time
value (if present).
unique_author
(optional) which takes an integer in minutes. This will limit voting to 1 vote per period. E.g.: Set it to 1440 to only vote for each author once a day.max_votes_per_post
(optional) which only votes n times per post (winfrey
mode only).only_tags
(optional) which only votes on posts that include these tags.vote_weight
if not present.favorite_accounts
) can now have individual vote percent.
inertia:100.00
)stream:false
will exit without streaming the blockchain. Useful in situations where you only want to replay:
and exit.Dr. Phil (drphil.rb
) is reimplementation of the "Winfrey" voting bot specification. The goal is to give everyone an upvote.
One optional improvement is that instead of voting 1% by 100 accounts like the Winfrey bot spec, this script can vote 100% with 1 randomly chosen account.
If the complaint about Winfrey is blockchain bloat, Dr. Phil prescribes weight loss to address this. But this feature would only work if there are enough voters defined in the script. If you plan to use this script for one or two accounts, you'll probably want to adjust the VOTE_WEIGHT
constant to something a bit lower.
To use this Radiator bot:
$ sudo apt-get update
$ sudo apt-get install ruby-full git openssl libssl1.0.0 libssl-dev
$ sudo apt-get upgrade
$ gem install bundler
$ gem install bundler
I've tested it on various versions of ruby. The oldest one I got it to work was:
ruby 2.0.0p645 (2015-04-13 revision 50299) [x86_64-darwin14.4.0]
First, clone this gist and install the dependencies:
$ git clone https://gist.github.com/61bcc2b821aa5acb24f7fc88921950c7.git drphil
$ cd drphil
$ bundle install
Then run it:
$ ruby drphil.rb
Dr. Phil will now do it's thing. Check here to see an updated version of this bot:
https://gist.github.com/inertia186/61bcc2b821aa5acb24f7fc88921950c7
Typically, you can upgrade to the latest version by this command, from the original directory you cloned into:
$ git pull
Usually, this works fine as long as you haven't modified anything. If you get an error, try this:
$ git stash --all
$ git pull --rebase
$ git stash pop
If you're still having problems, I suggest starting a new clone.
drphil.yml:1: syntax error, unexpected ':', expecting end-of-input
ruby drphil.yml
but you should run ruby drphil.rb
.Unable to vote with <account>. Invalid version
Make sure the .yml
file voter
items have the account name, followed by a space, followed by the account's WIF posting key. Also make sure you have removed the example accounts (social
and bad.account
are just for testing).
Is there a list of nodes?
https://ripplerm.github.io/steem-servers/
See my previous Ruby How To posts in: /f/ruby
If you're using Dr. Phil, I'd love to hear from you. Drop me a line and tell me what you think! I'm @inertia on STEEM and SteemSpeak.
I don't believe in intellectual "property". If you do, consider Dr. Phil as licensed under a Creative Commons License.
voting_rules:
# mode: drphil - picks one random voter for each post
# mode: winfrey - cycles all voters on each post
mode: drphil
vote_weight: 100.00 %
favorites_vote_weight: 100.00 %
following_vote_weight: 100.00 %
followers_vote_weight: 100.00 %
enable_comments: false
only_first_posts: false
only_fully_powered_up: false
min_wait: 18
max_wait: 30
min_rep: 25.0
max_rep: 99.9
min_voting_power: 25.00 %
# unique_author: 1440
# max_votes_per_post: 10
voters:
- social 5JrvPrQeBBvCRdjv29iDvkwn3EQYZ9jqfAHzrCyUvfbEbRkrYFC
- bad.account 5XXXBadWifXXXdjv29iDvkwn3EQYZ9jqfAHzrCyUvfbEbRkrYFC
favorite_accounts: inertia banjo
skip_accounts: leeroy.jenkins the.masses danlarimer ned-reddit-login
skip_tags: nsfw test ru--mat bm-open
# only_tags: golos
flag_signals: cheetah kulturagolosa
vote_signals:
:chain_options:
:chain: golos
:url: https://ws.golos.io
# Dr. Phil (drphil) is reimplmentation of the winfrey voting bot. The goal is
# to give everyone an upvote. But instead of voting 1% by 100 accounts like
# winfrey, this script will vote 100% with 1 randomly chosen account.
#
# See: https://steemit.com/radiator/@inertia/drphil-rb-voting-bot
require 'rubygems'
require 'bundler/setup'
require 'yaml'
Bundler.require
# If there are problems, this is the most time we'll wait (in seconds).
MAX_BACKOFF = 12.8
VOTE_RECHARGE_PER_DAY = 20.0
VOTE_RECHARGE_PER_HOUR = VOTE_RECHARGE_PER_DAY / 24
VOTE_RECHARGE_PER_MINUTE = VOTE_RECHARGE_PER_HOUR / 60
VOTE_RECHARGE_PER_SEC = VOTE_RECHARGE_PER_MINUTE / 60
@config_path = __FILE__.sub(/\.rb$/, '.yml')
unless File.exist? @config_path
puts "Unable to find: #{@config_path}"
exit
end
def parse_voters(voters)
case voters
when String
raise "Not found: #{voters}" unless File.exist? voters
f = File.open(voters)
hash = {}
f.read.each_line do |pair|
key, value = pair.split(' ')
hash[key] = value if !!key && !!hash
end
hash
when Array
a = voters.map{ |v| v.split(' ')}.flatten.each_slice(2)
return a.to_h if a.respond_to? :to_h
hash = {}
voters.each_with_index do |e|
key, val = e.split(' ')
hash[key] = val
end
hash
else; raise "Unsupported voters: #{voters}"
end
end
def parse_list(list)
if !!list && File.exist?(list)
f = File.open(list)
elements = []
f.each_line do |line|
elements += line.split(' ')
end
elements.uniq.reject(&:empty?).reject(&:nil?)
else
list.to_s.split(' ')
end
end
@config = YAML.load_file(@config_path)
rules = @config['voting_rules']
@voting_rules = {
mode: rules['mode'] || 'drphil',
vote_weight: (((rules['vote_weight'] || '100.0 %').to_f) * 100).to_i,
favorites_vote_weight: (((rules['favorites_vote_weight'] || rules['vote_weight'] || '100.0 %').to_f) * 100).to_i,
following_vote_weight: (((rules['following_vote_weight'] || rules['vote_weight'] || '100.0 %').to_f) * 100).to_i,
followers_vote_weight: (((rules['followers_vote_weight'] || rules['vote_weight'] || '100.0 %').to_f) * 100).to_i,
enable_comments: rules['enable_comments'],
only_first_posts: rules['only_first_posts'],
only_fully_powered_up: rules['only_fully_powered_up'],
min_wait: rules['min_wait'].to_i,
max_wait: rules['max_wait'].to_i,
min_rep: (rules['min_rep'] || 25.0),
max_rep: (rules['max_rep'] || 99.9).to_f,
min_voting_power: (((rules['min_voting_power'] || '0.0 %').to_f) * 100).to_i,
unique_author: rules['unique_author'],
max_votes_per_post: rules['max_votes_per_post'],
}
@voting_rules[:wait_range] = [@voting_rules[:min_wait]..@voting_rules[:max_wait]]
unless @voting_rules[:min_rep] =~ /dynamic:[0-9]+/
@voting_rules[:min_rep] = @voting_rules[:min_rep].to_f
end
@voting_rules = Struct.new(*@voting_rules.keys).new(*@voting_rules.values)
@voters = parse_voters(@config['voters'])
@favorite_accounts = parse_list(@config['favorite_accounts'])
@skip_accounts = parse_list(@config['skip_accounts'])
@skip_tags = parse_list(@config['skip_tags'])
@only_tags = parse_list(@config['only_tags'])
@skip_apps = parse_list(@config['skip_apps'])
@only_apps = parse_list(@config['only_apps'])
@flag_signals = parse_list(@config['flag_signals'])
@vote_signals = parse_list(@config['vote_signals'])
@favorite_account_weights = @favorite_accounts.map do |account|
pair = account.split(':')
next unless pair.size == 2
pair[1] = (pair[1].to_f * 100).to_i
pair
end.compact.to_h
@favorite_accounts = @favorite_accounts.map do |account|
account.split(':').first
end
@chain_options = @config[:chain_options]
@chain_options[:chain] = @chain_options[:chain].to_sym
@chain_options[:logger] = Logger.new(__FILE__.sub(/\.rb$/, '.log'))
def winfrey?; @voting_rules.mode == 'winfrey'; end
def drphil?; @voting_rules.mode == 'drphil'; end
def seinfeld?; @voting_rules.mode == 'seinfeld'; end
if (
!seinfeld? &&
@voting_rules.vote_weight == 0 && @voting_rules.favorites_vote_weight == 0 &&
@voting_rules.following_vote_weight == 0 && @voting_rules.followers_vote_weight == 0
)
puts "WARNING: All vote weights are zero. This is a bot that does nothing."
@voting_rules.mode = 'seinfeld'
end
@voted_for_authors = {}
@voting_power = {}
@threads = {}
@semaphore = Mutex.new
def to_rep(raw)
raw = raw.to_i
neg = raw < 0
level = Math.log10(raw.abs)
level = [level - 9, 0].max
level = (neg ? -1 : 1) * level
level = (level * 9) + 25
level
end
def poll_voting_power
@semaphore.synchronize do
@api.get_accounts(@voters.keys) do |accounts, error|
if !!error
puts "Unable to query voters #{@voters.keys.join(', ')}: #{error}"
return
end
accounts.each do |account|
voting_power = account.voting_power / 100.0
last_vote_time = Time.parse(account.last_vote_time + 'Z')
voting_elapse = Time.now.utc - last_vote_time
current_voting_power = voting_power + (voting_elapse * VOTE_RECHARGE_PER_SEC)
wasted_voting_power = [current_voting_power - 100.0, 0.0].max
current_voting_power = ([100.0, current_voting_power].min * 100).to_i
if wasted_voting_power > 0
puts "\t#{account.name} wasted voting power: #{('%.2f' % wasted_voting_power)} %"
end
@voting_power[account.name] = current_voting_power
end
@min_voting_power = @voting_power.values.min
@max_voting_power = @voting_power.values.max
@average_voting_power = @voting_power.values.reduce(0, :+) / accounts.size
end
end
end
def summary_voting_power
poll_voting_power
vp = @average_voting_power / 100.0
summary = []
summary << if @voting_power.size > 1
"Average remaining voting power: #{('%.3f' % vp)} %"
else
"Remaining voting power: #{('%.3f' % vp)} %"
end
if @voting_power.size > 1 && @max_voting_power > @voting_rules.min_voting_power
vp = @max_voting_power / 100.0
summary << "highest account: #{('%.3f' % vp)} %"
end
vp = @voting_rules.min_voting_power / 100.0
summary << "recharging when below: #{('%.3f' % vp)} %"
summary.join('; ')
end
def voters_recharging
@voting_power.map do |voter, power|
voter if power < @voting_rules.min_voting_power
end.compact
end
def skip_tags_intersection?(json_metadata)
metadata = JSON[json_metadata || '{}'] rescue {}
tags = metadata['tags'] || [] rescue []
tags = [tags].flatten
(@skip_tags & tags).any?
end
def only_tags_intersection?(json_metadata)
return true if @only_tags.none? # not set, assume all tags intersect
metadata = JSON[json_metadata || '{}'] rescue {}
tags = metadata['tags'] || [] rescue []
tags = [tags].flatten
(@only_tags & tags).any?
end
def skip_app?(json_metadata)
metadata = JSON[json_metadata || '{}'] rescue {}
app = metadata['app'].to_s.split('/').first rescue 'unknown'
@skip_apps.include? app
end
def only_app?(json_metadata)
return true if @only_apps.none?
metadata = JSON[json_metadata || '{}'] rescue {}
app = metadata['app'].to_s.split('/').first rescue 'unknown'
@only_apps.include? app
end
def voted_for_authors
limit = if @voted_for_authors.empty?
10000
else
300
end
@semaphore.synchronize do
@voters.keys.each do |voter|
@api.get_account_history(voter, -limit, limit) do |result, error|
if !!error
puts "Unable to get account hitory for #{voter}: #{error}"
next
end
result.reverse.each do |i, tx|
op = tx['op']
next unless op[0] == 'vote'
timestamp = Time.parse(tx['timestamp'] + 'Z')
latest = @voted_for_authors[op[1]['author']]
if latest.nil? || latest < timestamp
@voted_for_authors[op[1]['author']] = timestamp
end
end
end
end
end
@voted_for_authors
end
def already_voted_for?(author, unique_author = @voting_rules.unique_author)
return false if unique_author.nil?
now = Time.now.utc
voted_in_threshold = []
voted_for_authors.each do |author, vote_at|
if now - vote_at < unique_author * 60
voted_in_threshold << author
end
end
return true if voted_in_threshold.include? author
false
end
def may_vote?(comment)
return false if !@voting_rules.enable_comments && !comment.parent_author.empty?
return false if @skip_tags.include? comment.parent_permlink
return false if skip_tags_intersection? comment.json_metadata
return false unless only_tags_intersection? comment.json_metadata
return false if @skip_accounts.include? comment.author
return false if skip_app? comment.json_metadata
return false unless only_app? comment.json_metadata
# We are checking if any voter can vote at all. If at least one voter has a
# non-zero vote_weight, return true. Otherwise, don't bother to even queue up
# a thread.
if @voters.keys.map { |voter| vote_weight(comment.author, voter) > 0.0 }.include? true
true
else
false
end
end
def min_trending_rep(limit)
begin
@semaphore.synchronize do
if @min_trending_rep.nil? || Random.rand(0..limit) == 13
puts "Looking up trending up to #{limit} posts."
@api.get_discussions_by_trending(tag: '', limit: limit) do |trending, error|
raise error.message if !!error
@min_trending_rep = trending.map do |c|
c.author_reputation.to_i
end.min
puts "Current minimum dynamic rep: #{('%.3f' % to_rep(@min_trending_rep))}"
end
end
end
rescue => e
puts "Warning: #{e}"
end
@min_trending_rep || 0
end
def skip?(comment, voters)
if comment.respond_to? :cashout_time # HF18
if (cashout_time = Time.parse(comment.cashout_time + 'Z')) < Time.now.utc
puts "Skipped, cashout time has passed (#{cashout_time}):\n\t@#{comment.author}/#{comment.permlink}"
return true
end
end
if !!@voting_rules.only_first_posts
begin
@semaphore.synchronize do
@api.get_accounts([comment.author]) do |account, error|
if !!error
puts "Unable to find first post for #{comment.author}: #{error}"
return true
end
if account.post_count > 1
puts "Skipped, not first post:\n\t@#{comment.author}/#{comment.permlink}"
return true
end
end
end
rescue => e
puts "Warning: #{e}"
return true
end
end
if !!@voting_rules.only_fully_powered_up
unless comment.percent_steem_dollars == 0
puts "Skipped, reward not fully powered up:\n\t@#{comment.author}/#{comment.permlink}"
return true
end
end
if comment.max_accepted_payout.split(' ').first == '0.000'
puts "Skipped, payout declined:\n\t@#{comment.author}/#{comment.permlink}"
return true
end
if voters.empty? && winfrey?
puts "Skipped, everyone already voted:\n\t@#{comment.author}/#{comment.permlink}"
return true
end
unless @favorite_accounts.include? comment.author
if @voting_rules.min_rep =~ /dynamic:[0-9]+/
limit = @voting_rules.min_rep.split(':').last.to_i
if (rep = comment.author_reputation.to_i) < min_trending_rep(limit)
# ... rep too low ...
puts "Skipped, due to low dynamic rep (#{('%.3f' % to_rep(rep))}):\n\t@#{comment.author}/#{comment.permlink}"
return true
end
else
if (rep = to_rep(comment.author_reputation)) < @voting_rules.min_rep
# ... rep too low ...
puts "Skipped, due to low rep (#{('%.3f' % rep)}):\n\t@#{comment.author}/#{comment.permlink}"
return true
end
end
if (rep = to_rep(comment.author_reputation)) > @voting_rules.max_rep
# ... rep too high ...
puts "Skipped, due to high rep (#{('%.3f' % rep)}):\n\t@#{comment.author}/#{comment.permlink}"
return true
end
end
downvoters = comment.active_votes.map do |v|
v.voter if v.percent < 0
end.compact
if (signal = downvoters & @flag_signals).any?
# ... Got a signal flag ...
puts "Skipped, flag signals (#{signals.join(' ')} flagged):\n\t@#{comment.author}/#{comment.permlink}"
return true
end
upvoters = comment.active_votes.map do |v|
v.voter if v.percent > 0
end.compact
if (signals = upvoters & @vote_signals).any?
# ... Got a signal vote ...
puts "Skipped, vote signals (#{signals.join(' ')} voted):\n\t@#{comment.author}/#{comment.permlink}"
return true
end
all_voters = comment.active_votes.map(&:voter)
if (all_voters & voters).any?
# ... Someone already voted (probably because post was edited) ...
puts "Skipped, already voted:\n\t@#{comment.author}/#{comment.permlink}"
return true
end
if already_voted_for?(comment.author)
# ... Already voted in timeframe ...
puts "Skipped, already voted for @#{comment.author} within #{@voting_rules.unique_author} minutes"
return true
end
false
end
def following?(voter, author)
@voters_following ||= {}
following = @voters_following[voter] || []
count = -1
if following.empty?
until count == following.size
count = following.size
@follow_api.get_following(voter, following.last, 'blog', 100) do |result, error|
if !!error
puts "Unable to get follows for #{voter}: #{error}"
end
following += result.map(&:following)
following = following.uniq
end
end
@voters_following[voter] = following
end
@voters_following[voter] = nil if Random.rand(0..999) == 13
following.include? author
end
def follower?(voter, author)
@voters_followers ||= {}
followers = @voters_followers[voter] || []
count = -1
if followers.empty?
until count == followers.size
count = followers.size
@follow_api.get_followers(voter, followers.last, 'blog', 100) do |result, error|
if !!error
puts "Unable to get followers for #{voter}: #{error}"
end
followers += result.map(&:follower)
followers = followers.uniq
end
end
@voters_followers[voter] = nil if Random.rand(0..999) == 13
@voters_followers[voter] = followers
end
followers.include? author
end
def vote_weight(author, voter)
@semaphore.synchronize do
if @favorite_accounts.include? author
if @favorite_account_weights.keys.include? author
@favorite_account_weights[author]
else
@voting_rules.favorites_vote_weight
end
elsif following? voter, author
@voting_rules.following_vote_weight
elsif follower? voter, author
@voting_rules.followers_vote_weight
else
@voting_rules.vote_weight
end
end
end
def vote(comment, wait_offset = 0)
votes_cast = 0
backoff = 0.2
slug = "@#{comment.author}/#{comment.permlink}"
@threads.each do |k, t|
@threads.delete(k) unless t.alive?
end
@semaphore.synchronize do
if @threads.size != @last_threads_size
print "Pending votes: #{@threads.size} ... "
@last_threads_size = @threads.size
end
end
if @threads.keys.include? slug
puts "Skipped, vote already pending:\n\t#{slug}"
return
end
@threads[slug] = Thread.new do
comment = @api.get_content(comment.author, comment.permlink) do |comment, error|
if !!error
puts error.message
return
end
comment
end
voters = if winfrey?
@voters.keys - comment.active_votes.map(&:voter) - voters_recharging
else
@voters.keys
end - voters_recharging
return if skip?(comment, voters)
if wait_offset == 0
timestamp = Time.parse(comment.created + ' Z')
now = Time.now.utc
wait_offset = now - timestamp
end
if (wait = (Random.rand(*@voting_rules.wait_range) * 60) - wait_offset) > 0
puts "Waiting #{wait.to_i} seconds to vote for:\n\t#{slug}"
sleep wait
@api.get_content(comment.author, comment.permlink) do |comment, error|
if !!error
puts "Unable to get comment @#{comment.author}/#{comment.permlink}: #{error}"
end
return if skip?(comment, voters)
end
else
puts "Catching up to vote for:\n\t#{slug}"
sleep 3
end
loop do
begin
break if voters.empty?
author = comment.author
permlink = comment.permlink
voter = voters.sample
weight = vote_weight(author, voter)
break if weight == 0.0
if (vp = @voting_power[voter].to_i) < @voting_rules.min_voting_power
vp = vp / 100.0
if @voters.size > 1
puts "Recharging #{voter} vote power (currently too low: #{('%.3f' % vp)} %)"
else
puts "Recharging vote power (currently too low: #{('%.3f' % vp)} %)"
end
end
wif = @voters[voter]
tx = Radiator::Transaction.new(@chain_options.merge(wif: wif))
puts "#{voter} voting for #{slug}"
vote = {
type: :vote,
voter: voter,
author: author,
permlink: permlink,
weight: weight
}
op = Radiator::Operation.new(vote)
tx.operations << op
response = tx.process(true)
if !!response.error
message = response.error.message
if message.to_s =~ /You have already voted in a similar way./
puts "\tFailed: duplicate vote."
voters -= [voter]
next
elsif message.to_s =~ /Can only vote once every 3 seconds./
if winfrey? || voters.size == 1
puts "\tRetrying: voting too quickly."
sleep 3
else
puts "\tSkipped: voting too quickly."
voters -= [voter]
end
next
elsif message.to_s =~ /Voting weight is too small, please accumulate more voting power or steem power./
puts "\tFailed: voting weight too small"
voters -= [voter]
next
elsif message.to_s =~ /STEEMIT_UPVOTE_LOCKOUT_HF17/
puts "\tFailed: upvote lockout (last twelve hours before payout)"
break
elsif message.to_s =~ /tapos_block_summary/
puts "Retrying: tapos_block_summary (?)"
redo
elsif message.to_s =~ /now < trx.expiration/
puts "Retrying: now < trx.expiration (?)"
redo
elsif message.to_s =~ /signature is not canonical/
puts "\tRetrying: signature was not canonical (bug in Radiator?)"
redo
end
raise message
end
puts "\tSuccess: #{response.result.to_json}"
votes_cast += 1
if winfrey?
# The winfrey mode keeps voting until there are no more voters of
# until max_votes_per_post is reached (if set)
if @voting_rules.max_votes_per_post.nil? || votes_cast < @voting_rules.max_votes_per_post
voters -= [voter]
next
else
puts "Max votes per post reached."
break
end
end
# The drphil mode only votes with one key per post.
break
rescue => e
puts "Pausing #{backoff} :: Unable to vote with #{voter}. #{e}"
voters -= [voter]
sleep backoff
backoff = [backoff * 2, MAX_BACKOFF].min
end
end
end
end
puts "Current mode: #{@voting_rules.mode}. Accounts voting: #{@voters.size}"
replay = 0
stream = true
ARGV.each do |arg|
if arg =~ /replay:[0-9]+/
replay = arg.split('replay:').last.to_i rescue 0
end
stream = false if arg == 'stream:false'
end
replay_threads = []
if replay > 0
replay_threads << Thread.new do
@api = Radiator::Api.new(@chain_options)
@follow_api = Radiator::FollowApi.new(@chain_options)
@stream = Radiator::Stream.new(@chain_options)
properties = @api.get_dynamic_global_properties.result
last_irreversible_block_num = properties.last_irreversible_block_num
block_number = last_irreversible_block_num - replay
puts "Replaying from block number #{block_number} ..."
@api.get_blocks(block_number..last_irreversible_block_num) do |block, number|
next unless !!block
timestamp = Time.parse(block.timestamp + ' Z')
now = Time.now.utc
elapsed = now - timestamp
block.transactions.each do |tx|
tx.operations.each do |type, op|
vote(op, elapsed.to_i) if type == 'comment' && may_vote?(op)
end
end
end
sleep 3
puts "Done replaying."
end
end
unless stream
replay_threads.map(&:join)
@threads.values.map(&:join)
exit
end
puts "Now waiting for new posts."
loop do
@api = Radiator::Api.new(@chain_options)
@follow_api = Radiator::FollowApi.new(@chain_options)
@stream = Radiator::Stream.new(@chain_options)
op_idx = 0
begin
puts summary_voting_power
@stream.operations(:comment) do |comment|
next unless may_vote? comment
if @max_voting_power < @voting_rules.min_voting_power
vp = @max_voting_power / 100.0
puts "Recharging vote power (currently too low: #{('%.3f' % vp)} %)"
end
vote(comment)
puts summary_voting_power
end
rescue => e
@api.shutdown
puts "Unable to stream on current node. Retrying in 5 seconds. Error: #{e}"
sleep 5
end
end
voting_rules:
# mode: drphil - picks one random voter for each post
# mode: winfrey - cycles all voters on each post
mode: drphil
vote_weight: 100.00 %
favorites_vote_weight: 100.00 %
following_vote_weight: 100.00 %
followers_vote_weight: 100.00 %
enable_comments: false
only_first_posts: false
only_fully_powered_up: false
min_wait: 18
max_wait: 30
min_rep: 25.0
max_rep: 99.9
min_voting_power: 25.00 %
# unique_author: 1440
# max_votes_per_post: 10
voters:
- social 5JrvPrQeBBvCRdjv29iDvkwn3EQYZ9jqfAHzrCyUvfbEbRkrYFC
- bad.account 5XXXBadWifXXXdjv29iDvkwn3EQYZ9jqfAHzrCyUvfbEbRkrYFC
favorite_accounts: inertia banjo
skip_accounts: leeroy.jenkins the.masses danlarimer ned-reddit-login
skip_tags: nsfw test
# only_tags: steemit
# only_apps: steemit esteem streemian pysteem steepshot busy chainbb banjo_bot chronicle steemq
# skip_apps: piston
flag_signals: cheetah steemcleaners
vote_signals:
:chain_options:
:chain: steem
:url: https://api.steemit.com