Class: RedditBot::Bot

Inherits:
Object
  • Object
show all
Defined in:
lib/reddit_bot.rb

Constant Summary collapse

@@skip_erroneous_descending_ids =
lambda do |array|
  array.reverse.each_with_object([]) do |item, result|
    unless result.empty?
      a, b = [item["id"], result.first["id"]]
      next if a == b || a.size < b.size || !(a.size > b.size) && a < b
    end
    result.unshift item
  end
end

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(secrets, **kwargs) ⇒ Bot

secrets

Hash with keys :client_id, :client_secret, :password: and :login

kwargs

keyword params may include :subreddit for clever methods



26
27
28
29
30
31
# File 'lib/reddit_bot.rb', line 26

def initialize secrets, **kwargs
  @name, @secret_password, @user_agent, *@secret_auth = secrets.values_at *%i{ login password user_agent client_id client_secret }
  # @ignore_captcha = true
  # @ignore_captcha = kwargs[:ignore_captcha] if kwargs.has_key?(:ignore_captcha)
  @subreddit = kwargs[:subreddit]
end

Instance Attribute Details

#nameObject (readonly)

bot’s Reddit username; set via constructor parameter secrets



22
23
24
# File 'lib/reddit_bot.rb', line 22

def name
  @name
end

Instance Method Details

#each_comment_of_the_post_thread(article) ⇒ Object

article

String ID36 of a post or self.post



182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/reddit_bot.rb', line 182

def each_comment_of_the_post_thread article
  Enumerator.new do |e|
    f = lambda do |smth|
      smth["data"]["children"].each do |child|
        f[child["data"]["replies"]] if child["data"]["replies"].is_a? Hash
        fail "unknown type child['kind']: #{child["kind"]}" unless child["kind"] == "t1"
        e << [child["data"]["name"], child["data"]]
      end
    end
    f[ json(:get, "/comments/#{article}", depth: 100500, limit: 100500).tap do |t|
      fail "smth weird about /comments/<id> response" unless t.size == 2
    end[1] ]
  end
end

#each_new_post_with_top_level_commentsObject

:yields: JSON objects: [“data”] part of post or self.post, top level comment ([“children”] element)



168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/reddit_bot.rb', line 168

def each_new_post_with_top_level_comments
  # TODO add keys assertion like in method above?
  json(:get, "/r/#{@subreddit}/new")["data"]["children"].each do |post|
    fail "unknown type post['kind']: #{post["kind"]}" unless post["kind"] == "t3"
    t = json :get, "/comments/#{post["data"]["id"]}", depth: 1, limit: 100500#, sort: "top"
    fail "smth weird about /comments/<id> response" unless t.size == 2
    yield post["data"], t[1]["data"]["children"].map{ |child|
      fail "unknown type child['kind']: #{child["kind"]}" unless child["kind"] == "t1"
      child["data"]
    }.to_enum
  end
end

#json(mtd, path, _form = []) ⇒ Object

mtd

Symbol :get or :post

path

String an API method

_form

Array or Hash API method params



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/reddit_bot.rb', line 36

def json mtd, path, _form = []
  form = Hash[_form]
  response = JSON.load resp_with_token mtd, path, form.merge({api_type: "json"})
  if response.is_a?(Hash) && response["json"] && # for example, flairlist.json and {"error": 403} do not have it
     !response["json"]["errors"].empty?
    Module.nesting[1].logger.error "ERROR OCCURED on #{[mtd, path]}"
    fail "unknown how to handle multiple errors" if 1 < response["json"]["errors"].size
    Module.nesting[1].logger.error "error: #{response["json"]["errors"]}"
    error, description = response["json"]["errors"].first
      case error
      when "ALREADY_SUB" ; Module.nesting[1].logger.warn "was rejected by moderator if you didn't see in dups"
      # when "BAD_CAPTCHA" ; update_captcha
      #   json mtd, path, form.merger( {
      #     iden: @iden_and_captcha[0],
      #     captcha: @iden_and_captcha[1],
      #   } ) unless @ignore_captcha
      when "RATELIMIT"
        fail error unless description[/\Ayou are doing that too much\. try again in (\d) minutes\.\z/]
        Module.nesting[1].logger.info "retrying in #{$1.to_i + 1} minutes"
        sleep ($1.to_i + 1) * 60
        return json mtd, path, _form
      else ; fail error
      end
  end
  response
end

#leave_a_comment(thing_id, text) ⇒ Object

thing_id

String fullname of a post (or self.post?), comment (and private message?)

text

:nodoc:



104
105
106
107
108
109
110
111
112
# File 'lib/reddit_bot.rb', line 104

def leave_a_comment thing_id, text
  Module.nesting[1].logger.warn "leaving a comment on '#{thing_id}'"
  json(:post, "/api/comment",
    thing_id: thing_id,
    text: text,
  ).tap do |result|
    fail result["json"]["errors"].to_s unless result["json"]["errors"].empty?
  end
end

#new_posts(subreddit = nil, caching = false) ⇒ Object

:yields: JSON objects: [“data”] part of post or self.post



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/reddit_bot.rb', line 134

def new_posts subreddit = nil, caching = false
  cache = lambda do |id, &block|
    next block.call unless caching
    require "fileutils"
    FileUtils.mkdir_p "cache"
    filename = "cache/#{Digest::MD5.hexdigest id.inspect}"
    next YAML.load File.read filename if File.exist? filename
    block.call.tap do |data|
      File.write filename, YAML.dump(data)
    end
  end
  Enumerator.new do |e|
    after = {}
    loop do
      # TODO maybe force lib user to prepend "r/" to @subreddit constructor?
      args = [:get, "/#{subreddit || (@subreddit ? "r/#{@subreddit}" : fail)}/new", {limit: 100}.merge(after)]
      result = cache.call(args){ json *args }
      fail if result.keys != %w{ kind data }
      fail if result["kind"] != "Listing"
      fail result["data"].keys.inspect unless result["data"].keys == %w{ after dist modhash whitelist_status children before } ||
                                              result["data"].keys == %w{ modhash dist children after before }
      @@skip_erroneous_descending_ids[ result["data"]["children"].map do |post|
        fail "unknown type post['kind']: #{post["kind"]}" unless post["kind"] == "t3"
        post["data"]
      end ].each do |data|
        e << data
      end
      break unless marker = result["data"]["after"]
      after = {after: marker}
    end
  end
end

#report(reason, thing_id) ⇒ Object

reason

:nodoc:

thing_id

String fullname of a “link, commenr or message”



77
78
79
80
81
82
83
# File 'lib/reddit_bot.rb', line 77

def report reason, thing_id
  Module.nesting[1].logger.warn "reporting '#{thing_id}'"
  json :post, "/api/report",
    reason: "other",
    other_reason: reason,
    thing_id: thing_id
end

#set_post_flair(post, link_flair_css_class, link_flair_text) ⇒ Object

post

JSON object of a post of self.post

link_flair_css_class

:nodoc:

link_flair_text

:nodoc:



88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/reddit_bot.rb', line 88

def set_post_flair post, link_flair_css_class, link_flair_text
  Module.nesting[1].logger.warn "setting flair '#{link_flair_css_class}' with text '#{link_flair_text}' to post '#{post["name"]}'"
  if {"error"=>403} == @flairselector_choices ||= json(:post, "/r/#{@subreddit}/api/flairselector", link: post["name"])
    Module.nesting[1].logger.error "possibly not enough permissions for /r/#{@subreddit}/api/flairselector"
    return
  end
  json :post, "/api/selectflair",
    link: post["name"],
    text: link_flair_text,
    flair_template_id: @flairselector_choices["choices"].find{ |i| i["flair_css_class"] == link_flair_css_class }.tap{ |flair|
      fail "can't find '#{link_flair_css_class}' flair class at https://www.reddit.com/r/#{@subreddit}/about/flair/#link_templates" unless flair
    }["flair_template_id"]
end