cosgrove 0.0.1rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ require 'phantomjs'
2
+
3
+ # See: http://www.thegreatcodeadventure.com/screen-capture-in-rails-with-phantomjs/
4
+ module Cosgrove
5
+ module Phantomjs
6
+ PATH_TO_PHANTOM_SCRIPT = "#{File.dirname(__FILE__)}/../../support/js/screencap.js"
7
+
8
+ def take_screencap(url, filename = nil, width = 64, height = 64)
9
+ target_path = Digest::MD5.hexdigest(filename || url.parameterize)
10
+ target_path += '.png'
11
+
12
+ Dir.chdir('/tmp')
13
+ system "phantomjs #{PATH_TO_PHANTOM_SCRIPT} \"#{url}\" #{target_path} #{width} #{height}"
14
+
15
+ File.open(target_path)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,52 @@
1
+ require 'steem-slap'
2
+
3
+ module Cosgrove
4
+ module SnarkCommands
5
+ WITTY = [
6
+ "Who set us up the TNT?",
7
+ "Everything's going to plan. No, really, that was supposed to happen.",
8
+ "Uh... Did I do that?",
9
+ "Oops.",
10
+ "Why did you do that?",
11
+ "I feel sad now :(",
12
+ "My bad.",
13
+ "I'm sorry, Dave.",
14
+ "I let you down. Sorry :(",
15
+ "On the bright side, I bought you a teddy bear!",
16
+ "Daisy, daisy...",
17
+ "Oh - I know what I did wrong!",
18
+ "Hey, that tickles! Hehehe!",
19
+ "I blame inertia.",
20
+ "You should try our sister blockchain, GOLOS!",
21
+ "Don't be sad. I'll do better next time, I promise!",
22
+ "Don't be sad, have a hug! <3",
23
+ "I just don't know what went wrong :(",
24
+ "Shall we play a game?",
25
+ "Quite honestly, I wouldn't worry myself about that.",
26
+ "I bet Cylons wouldn't have this problem.",
27
+ "Sorry :(",
28
+ "Surprise! Haha. Well, this is awkward.",
29
+ "Would you like a cupcake?",
30
+ "Hi. I'm cosgrove, and I'm a crashaholic.",
31
+ "Ooh. Shiny.",
32
+ "This doesn't make any sense!",
33
+ "Why is it breaking :(",
34
+ "Don't do that.",
35
+ "Ouch. That hurt :(",
36
+ "You're mean.",
37
+ "This is a token for 1 free hug. Redeem at your nearest Steemian: `[~~HUG~~]`",
38
+ "There are four lights!",
39
+ "Witty comment unavailable :("
40
+ ]
41
+
42
+ def self.add_all_snark_commands(bot)
43
+ bot.command :slap do |_event, *target|
44
+ if target.any?
45
+ "*#{SteemSlap.slap(target.join(' '))}*"
46
+ else
47
+ "There are #{SteemSlap::combinations} slap combinations, see: https://gist.github.com/inertia186/c34e6e7b73f7ee9fb5f60f5ed8f30206"
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,213 @@
1
+ module Cosgrove
2
+ module Support
3
+ include Cosgrove::Utils
4
+ include ActionView::Helpers::TextHelper
5
+ include ActionView::Helpers::DateHelper
6
+
7
+ def suggest_account_name(account_name)
8
+ regex = /.*#{account_name.chars.each.map { |c| c }.join('.*')}.*/
9
+ guesses = SteemData::Account.where(name: regex).distinct(:name)
10
+
11
+ if guesses.any?
12
+ guesses.sample
13
+ end
14
+ end
15
+
16
+ def unknown_account(account_name, event = nil)
17
+ help = ["Unknown account: *#{account_name}*"]
18
+ event.channel.start_typing if !!event
19
+ guess = suggest_account_name(account_name)
20
+
21
+ help << ", did you mean: #{guess}?" if !!guess
22
+
23
+ if !!event
24
+ event.respond help.join
25
+ return
26
+ end
27
+
28
+ help.join
29
+ end
30
+
31
+ def mongo_behind_warning(event)
32
+ begin
33
+ message = []
34
+
35
+ if (blocks = head_block_number(:steem) - steem_data_head_block_number) > 1200
36
+ elapse = blocks * 3
37
+ message << "Mongo is behind by #{time_ago_in_words(elapse.seconds.ago)}."
38
+ end
39
+
40
+ if message.size > 0
41
+ event.respond message.join(' ')
42
+ end
43
+ rescue => e
44
+ event.respond "Mongo might be behind, but the API is also acting up. Please try again later.\n\n```#{e.inspect}\n```"
45
+ sleep 15
46
+ event.respond Cosgrove::SnarkCommands::WITTY.sample
47
+ end
48
+ end
49
+
50
+ def cannot_find_input(event, message_prefix = "Unable to find that.")
51
+ message = [message_prefix]
52
+
53
+ message << if (blocks = head_block_number(:steem) - steem_data_head_block_number) > 10
54
+ elapse = blocks * 3
55
+ " Mongo is behind by #{time_ago_in_words(elapse.seconds.ago)}. Try again later."
56
+ else
57
+ " Mongo might be behind or this is not a valid input."
58
+ end
59
+
60
+ event.respond message.join(' ')
61
+ end
62
+
63
+ def append_link_details(event, slug)
64
+ author_name, permlink = parse_slug slug
65
+
66
+ post = SteemData::Post.where(author: author_name, permlink: permlink).last
67
+
68
+ if post.nil?
69
+ # Fall back to RPC
70
+ response = api(:steem).get_content(author_name, permlink)
71
+ unless response.result.author.empty?
72
+ post = response.result
73
+ created = Time.parse(post.created + 'Z')
74
+ cashout_time = Time.parse(post.cashout_time + 'Z')
75
+ end
76
+ end
77
+
78
+ return if post.nil?
79
+
80
+ created ||= post.created
81
+ cashout_time ||= post.cashout_time
82
+
83
+ details = []
84
+ age = time_ago_in_words(created)
85
+ age = age.slice(0, 1).capitalize + age.slice(1..-1)
86
+
87
+ details << if created < 30.minutes.ago
88
+ "#{age} old"
89
+ else
90
+ "**#{age}** old"
91
+ end
92
+
93
+ if post.active_votes.any?
94
+ upvotes = post.active_votes.map{ |v| v if v['percent'] > 0 }.compact.count
95
+ downvotes = post.active_votes.map{ |v| v if v['percent'] < 0 }.compact.count
96
+ netvotes = upvotes - downvotes
97
+ details << "Net votes: #{netvotes}"
98
+
99
+ # Only append this detail of the post less than an hour old.
100
+ if created > 1.hour.ago
101
+ votes = SteemData::AccountOperation.type('vote').starting(post.created)
102
+ total_votes = votes.count
103
+ total_voters = votes.distinct(:voter).size
104
+
105
+ if total_votes > 0 && total_voters > 0
106
+ details << "Out of #{pluralize(total_votes - netvotes, 'vote')} cast by #{pluralize(total_voters, 'voter')}"
107
+ end
108
+ end
109
+ end
110
+
111
+ details << "Comments: #{post.children.to_i}"
112
+
113
+ page_views = page_views("/#{post.parent_permlink}/@#{post.author}/#{post.permlink}")
114
+ details << "Views: #{page_views}" if !!page_views
115
+
116
+ event.respond details.join('; ')
117
+
118
+ return nil
119
+ end
120
+
121
+ def find_account(key, event = nil, chain = :steem)
122
+ key = key.to_s.downcase
123
+
124
+ if (accounts = SteemData::Account.where(name: key)).any?
125
+ return accounts.first
126
+ elsif !!(cb_account = Cosgrove::Account.find_by_discord_id(key, chain))
127
+ return SteemData::Account.find_by(name: cb_account.account_name)
128
+ else
129
+ # Fall back to RPC
130
+ if !!key
131
+ response = api(chain).get_accounts([key])
132
+ return account = response.result.first
133
+ end
134
+
135
+ unknown_account(key, event) unless !!account
136
+ end
137
+ end
138
+
139
+ def page_views(uri)
140
+ begin
141
+ @agent ||= Cosgrove::Agent.new
142
+ page = @agent.get("https://steemit.com#{uri}")
143
+
144
+ _uri = URI.parse('https://steemit.com/api/v1/page_view')
145
+ https = Net::HTTP.new(_uri.host,_uri.port)
146
+ https.use_ssl = true
147
+ request = Net::HTTP::Post.new(_uri.path)
148
+ request.initialize_http_header({
149
+ 'Cookie' => @agent.cookies.join('; '),
150
+ 'accept' => 'application/json',
151
+ 'Accept-Encoding' => 'gzip, deflate, br',
152
+ 'Accept-Language' => 'en-US,en;q=0.8',
153
+ 'Connection' => 'keep-alive',
154
+ 'content-type' => 'text/plain;charset=UTF-8',
155
+ 'Host' => 'steemit.com',
156
+ 'Origin' => 'https://steemit.com'
157
+ })
158
+
159
+ csrf = page.parser.to_html.split(',"csrf":"').last.split('","new_visit":').first
160
+ # Uncomment in case views stop showing.
161
+ # puts "DEBUG: #{csrf}"
162
+ return unless csrf.size == 36
163
+
164
+ post_data = {
165
+ csrf: csrf,
166
+ page: uri
167
+ }
168
+ request.set_form_data(post_data)
169
+ response = https.request(request)
170
+ JSON[response.body]['views']
171
+ rescue => e
172
+ puts "Attempting to get page_view failed: #{e}"
173
+ end
174
+ end
175
+
176
+ def last_irreversible_block(chain = :steem)
177
+ seconds_ago = (head_block_number(chain) - last_irreversible_block_num(chain)) * 3
178
+
179
+ "Last Irreversible Block was #{time_ago_in_words(seconds_ago.seconds.ago)} ago."
180
+ end
181
+
182
+ def send_url(event, url)
183
+ open(url) do |f|
184
+ tempfile = Tempfile.new(['send_url', ".#{url.split('.').last}"])
185
+ tempfile.binmode
186
+ tempfile.write(f.read)
187
+ tempfile.close
188
+ event.send_file File.open tempfile.path
189
+ end
190
+ end
191
+
192
+ def muted(options = {})
193
+ [] if options.empty?
194
+ by = [options[:by]].flatten
195
+ chain = options[:chain]
196
+ muted = []
197
+
198
+ by.each do |a|
199
+ ignoring = []
200
+ count = -1
201
+ until count == ignoring.size
202
+ count = ignoring.size
203
+ response = follow_api(chain).get_following(a, ignoring.last, 'ignore', 100)
204
+ ignoring += response.result.map(&:following)
205
+ ignoring = ignoring.uniq
206
+ end
207
+ muted += ignoring
208
+ end
209
+
210
+ muted.uniq
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,147 @@
1
+ module Cosgrove
2
+ class UpvoteJob
3
+ include Cosgrove::Utils
4
+ include Cosgrove::Support
5
+ include Cosgrove::Config
6
+
7
+ def initialize(options = {})
8
+ @on_success = options[:on_success]
9
+ end
10
+
11
+ def perform(event, slug)
12
+ if slug.nil? || slug.empty?
13
+ event.respond 'Sorry, I wasn\'t paying attention.'
14
+ return
15
+ end
16
+
17
+ author_name, permlink = parse_slug slug
18
+ discord_id = event.author.id
19
+ cb_account = Cosgrove::Account.find_by_discord_id(discord_id)
20
+ registered = !!cb_account
21
+ muted = muted by: steem_account, chain: :steem
22
+
23
+ posts = SteemData::Post.root_posts.where(author: author_name, permlink: permlink)
24
+ votes_today = SteemData::AccountOperation.type('vote').where(voter: steem_account).today
25
+ today_count = votes_today.count
26
+ author_count = votes_today.where(author: author_name).count
27
+ vote_ratio = if today_count == 0
28
+ 0.0
29
+ else
30
+ author_count.to_f / today_count
31
+ end
32
+
33
+ post = posts.first
34
+
35
+ if post.nil?
36
+ # Fall back to RPC
37
+ response = api(:steem).get_content(author_name, permlink)
38
+ unless response.result.author.empty?
39
+ post = response.result
40
+ created = Time.parse(post.created + 'Z')
41
+ cashout_time = Time.parse(post.cashout_time + 'Z')
42
+ end
43
+ end
44
+
45
+ if post.nil?
46
+ cannot_find_input(event)
47
+ return
48
+ end
49
+
50
+ created ||= post.created
51
+ cashout_time ||= post.cashout_time
52
+
53
+ nope = if created > 5.minutes.ago
54
+ "Give it a second! It's going to SPACE! Can you give it a second to come back from space?"
55
+ elsif cashout_time < Time.now.utc
56
+ 'Unable to vote on that. Too old.'
57
+ elsif post.parent_permlink == 'nsfw'
58
+ puts "Won't vote because parent_permlink: nsfw"
59
+ 'Unable to vote on that.'
60
+ elsif post.json_metadata.include?('nsfw')
61
+ puts "Won't vote because json_metadata includes: nsfw"
62
+ 'Unable to vote on that.'
63
+ elsif post.active_votes.map{ |v| v['voter'] }.include?('blacklist-a')
64
+ puts "Won't vote blacklist-a voted."
65
+ 'Unable to vote on that.'
66
+ elsif (rep = to_rep(post.author_reputation).to_f) < 25.0
67
+ puts "Won't vote because rep too low: #{rep}"
68
+ 'Unable to vote on that.'
69
+ elsif muted.include? author_name
70
+ puts "Won't vote because author muted."
71
+ 'Unable to vote.'
72
+ elsif !registered
73
+ 'Unable to vote. Feature resticted to registered users.'
74
+ elsif cb_account.novote?
75
+ 'Unable to vote. Your account has been resticted.'
76
+ elsif today_count > 10 && vote_ratio > 0.1
77
+ "Maybe later. It seems like I've been voting for #{author_name} quite a bit lately."
78
+ elsif post.active_votes.map{ |v| v['voter'] }.include?(steem_account)
79
+ title = post.title
80
+ title = post.permlink if title.empty?
81
+ "I already voted on #{title} by #{post.author}."
82
+ end
83
+
84
+ if !!nope
85
+ event.respond nope
86
+ return
87
+ end
88
+
89
+ vote = {
90
+ type: :vote,
91
+ voter: steem_account,
92
+ author: post.author,
93
+ permlink: post.permlink,
94
+ weight: upvote_weight(event.channel.id)
95
+ }
96
+
97
+ tx = new_tx :steem
98
+ op = Radiator::Operation.new(vote)
99
+ tx.operations << op
100
+ response = tx.process(true)
101
+
102
+ ap response.to_json
103
+
104
+ if !!response.error
105
+ 'Unable to vote right now. Maybe I already voted on that. Try again later.'
106
+ elsif !!response.result.id
107
+ if created > 30.minutes.ago
108
+ event.respond "*#{SteemSlap.slap(event.author.display_name)}*"
109
+ end
110
+
111
+ if !!@on_success
112
+ begin
113
+ @on_success.call(event, "@#{post.author}/#{post.permlink}")
114
+ rescue => e
115
+ ap e
116
+ ap e.backtrace
117
+ end
118
+ end
119
+
120
+ "Upvoted: #{post.title} by #{author_name}"
121
+ else
122
+ ':question:'
123
+ end
124
+ end
125
+ private
126
+ def upvote_weight(channel_id = nil)
127
+ upvote_weight = cosgrove_upvote_weight
128
+
129
+ case upvote_weight
130
+ when 'dynamic'
131
+ bot_account = find_account(steem_account)
132
+ upvote_weight = bot_account.voting_power.to_i
133
+ when 'upvote_rules'
134
+ upvote_weight = channel_upvote_weight(channel_id)
135
+
136
+ if upvote_weight == 'dynamic'
137
+ bot_account = find_account(steem_account)
138
+ upvote_weight = bot_account.voting_power.to_i
139
+ else
140
+ upvote_weight = (((upvote_weight || '100.0 %').to_f) * 100).to_i
141
+ end
142
+ else
143
+ upvote_weight = (((upvote_weight || '100.0 %').to_f) * 100).to_i
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,116 @@
1
+ module Cosgrove
2
+ module Utils
3
+ include Cosgrove::Config
4
+
5
+ def reset_api
6
+ @steem_api = @golos_api = @test_api = nil
7
+ @steem_folow_api = @golos_follow_api = @test_folow_api = nil
8
+ @cycle_api_at = nil
9
+ end
10
+
11
+ def ping_api(chain)
12
+ response = api(chain).get_config
13
+
14
+ reset_api if !!response.error
15
+
16
+ result = response.result
17
+ reset_api unless result.keys.any?
18
+
19
+ true
20
+ end
21
+
22
+ def api(chain)
23
+ reset_api if @cycle_api_at.nil? || @cycle_api_at < 15.minutes.ago
24
+
25
+ @cycle_api_at ||= Time.now
26
+
27
+ case chain
28
+ when :steem then @steem_api ||= Radiator::Api.new(chain: :steem, url: steem_api_url)
29
+ when :golos then @golos_api ||= Radiator::Api.new(chain: :golos, url: golos_api_url)
30
+ when :test then @test_api ||= Radiator::Api.new(chain: :test, url: test_api_url)
31
+ end
32
+ end
33
+
34
+ def follow_api(chain)
35
+ reset_api if @cycle_api_at.nil? || @cycle_api_at < 15.minutes.ago
36
+
37
+ @cycle_api_at ||= Time.now
38
+
39
+ case chain
40
+ when :steem then @steem_follow_api ||= Radiator::FollowApi.new(chain: :steem, url: steem_api_url)
41
+ when :golos then @golos_follow_api ||= Radiator::FollowApi.new(chain: :golos, url: golos_api_url)
42
+ when :test then @test_follow_api ||= Radiator::FollowApi.new(chain: :test, url: test_api_url)
43
+ end
44
+ end
45
+
46
+ def steem_data_head_block_number
47
+ SteemData::Setting.last.last_block
48
+ end
49
+
50
+ def properties(chain)
51
+ api(chain).get_dynamic_global_properties.result
52
+ end
53
+
54
+ def head_block_number(chain)
55
+ properties(chain)['head_block_number']
56
+ end
57
+
58
+ def last_irreversible_block_num(chain)
59
+ properties(chain)['last_irreversible_block_num']
60
+ end
61
+
62
+ def new_tx(chain)
63
+ case chain
64
+ when :steem then Radiator::Transaction.new(chain: :steem, wif: steem_posting_wif, url: steem_api_url)
65
+ when :golos then Radiator::Transaction.new(chain: :golos, wif: golos_posting_wif, url: golos_api_url)
66
+ when :test then Radiator::Transaction.new(chain: :test, wif: test_posting_wif, url: test_api_url)
67
+ end
68
+ end
69
+
70
+ def to_rep(raw)
71
+ raw = raw.to_i
72
+ neg = raw < 0
73
+ level = Math.log10(raw.abs)
74
+ level = [level - 9, 0].max
75
+ level = (neg ? -1 : 1) * level
76
+ level = (level * 9) + 25
77
+
78
+ '%.2f' % level
79
+ end
80
+
81
+ def parse_slug(slug)
82
+ case slug
83
+ when /^latest:/ then find_author_name_permlink(slug)
84
+ when /^first:/ then find_author_name_permlink(slug)
85
+ else
86
+ slug = slug.split('@').last
87
+ author_name = slug.split('/')[0]
88
+ permlink = slug.split('/')[1..-1].join('/')
89
+
90
+ [author_name, permlink]
91
+ end
92
+ end
93
+
94
+ def find_author_name_permlink(slug)
95
+ op, author_name = slug.split(':')
96
+ author_name, offset = author_name.split(/[\+-]/)
97
+ author = find_account(author_name)
98
+
99
+ offset = offset.to_i
100
+
101
+ posts = if op == 'latest'
102
+ SteemData::Post.root_posts.where(author: author.name).order(created: :desc)
103
+ elsif op == 'first'
104
+ SteemData::Post.root_posts.where(author: author.name).order(created: :asc)
105
+ else
106
+ []
107
+ end
108
+
109
+ if posts.any? && !!(post = posts[offset.to_i.abs])
110
+ return [author_name, post.permlink]
111
+ end
112
+
113
+ []
114
+ end
115
+ end
116
+ end