Permalink
Browse files

Various stylistic changes.

  • Loading branch information...
1 parent bbce97f commit 087963bd3d8937bd6b3068f82196862888d0344f andkerosine committed Feb 27, 2012
View
File renamed without changes.
View
@@ -0,0 +1,5 @@
+{
+ "auth": {
+ },
+ "delay": 2
+}
View
@@ -1,38 +1,86 @@
-%w[net/http/persistent json].each { |d| require d }
+%w[json net/http/persistent].each { |dep| require dep }
+CONFIG_FILE = '.snooby/config.json'
+
+# Creates and initializes configuration and caching on a per-directory basis.
+# Doesn't update if they already exist, but this might need to be fleshed out
+# a bit in future to merge values to be kept with new defaults in upgrades.
+unless File.exists? CONFIG_FILE
+ DEFAULT_CONFIG = File.join Gem.datadir('snooby'), 'config.json'
+ %w[.snooby .snooby/cache].each { |dir| Dir.mkdir dir }
+ File.open(CONFIG_FILE, 'w') { |file| file << File.read(DEFAULT_CONFIG) }
+end
+
+# reddit API (Snoo) + happy programming (Ruby) = Snooby
module Snooby
+
+ class << self
+ attr_accessor :config, :active
+ end
+
+ # Raised with a pretty print of the relevant JSON object whenever an API call
+ # returns a non-empty "errors" array, typically in cases of rate limiting and
+ # missing or insufficient authorization. Also used as a general container for
+ # miscellaneous errors related to site functionality.
+ class RedditError < StandardError
+ end
+
+ # Changes to configuration should persist across multiple uses. This class is
+ # a simple modification to the standard hash's setter that updates the config
+ # file whenever a value changes.
+ class Config < Hash
+ def []=(key, value)
+ super
+ return unless Snooby.config # nil during initialization inside JSON#parse
+ File.open(CONFIG_FILE, 'w') do |file|
+ file << JSON.pretty_generate(Snooby.config)
+ end
+ end
+ end
+
+ @config = JSON.parse(File.read(CONFIG_FILE), object_class: Config)
+ raise RedditError, 'Insufficiently patient delay.' if @config['delay'] < 2
+
# Opens a persistent connection that provides a significant speed improvement
# during repeated calls; reddit's two-second rule pretty much nullifies this,
# but it's still a great library and persistent connections are a Good Thing.
- Conn = Net::HTTP::Persistent.new('snooby')
+ Conn = Net::HTTP::Persistent.new 'Snooby'
- # Provides a mapping of things and actions to their respective URL fragments.
- # A path is eventually used as a complete URI, thus the merge.
paths = {
:comment => 'api/comment',
:compose => 'api/compose',
:delete => 'api/del',
:disliked => 'user/%s/disliked.json',
+ :domain_posts => 'domain/%s.json',
:friend => 'api/friend',
:hidden => 'user/%s/hidden.json',
+ :hide => 'api/hide',
:liked => 'user/%s/liked.json',
:login => 'api/login/%s',
+ :mark => 'api/marknsfw',
:me => 'api/me.json',
:post_comments => 'comments/%s.json',
:reddit => '.json',
+ :save => 'api/save',
:saved => 'saved.json',
+ :submit => 'api/submit',
:subreddit_about => 'r/%s/about.json',
:subreddit_comments => 'r/%s/comments.json',
:subreddit_posts => 'r/%s.json',
:subscribe => 'api/subscribe',
:unfriend => 'api/unfriend',
+ :unhide => 'api/unhide',
+ :unmark => 'api/unmarknsfw',
+ :unsave => 'api/unsave',
:user => 'user/%s',
:user_about => 'user/%s/about.json',
:user_comments => 'user/%s/comments.json',
:user_posts => 'user/%s/submitted.json',
- :vote => 'api/vote'
+ :vote => 'api/vote',
}
- Paths = paths.merge(paths) { |k, v| "http://www.reddit.com/#{v}" }
+
+ # Provides a mapping of things and actions to their respective URL fragments.
+ Paths = paths.merge(paths) { |act, path| "http://www.reddit.com/#{path}" }
# Provides a mapping of things to a list of all the attributes present in the
# relevant JSON object. A lot of these probably won't get used too often, but
@@ -42,20 +90,34 @@ module Snooby
:post => %w[author author_flair_css_class author_flair_text clicked created created_utc domain downs hidden id is_self likes media media_embed name num_comments over_18 permalink saved score selftext subreddit subreddit_id thumbnail title ups url]
}
- # Wraps the connection created above for both POST and GET requests to ensure
- # that the two-second rule is adhered to. The uri parameter is turned into an
- # actual URI once here instead of all over the place. The client's modhash is
- # always required for POST requests, so it is passed along by default.
+ # Wraps the connection for all requests to ensure that the two-second rule is
+ # adhered to. The uri parameter comes in as a string because it may have been
+ # susceptible to variables, so it gets turned into an actual URI here instead
+ # of all over the place. Since it's required for all POST requests other than
+ # logging in, the client's modhash is sent along by default.
def self.request(uri, data = nil)
- uri = URI(uri)
- if data
- data.merge!(:uh => Snooby.active.uh) if Snooby.active
- post = Net::HTTP::Post.new(uri.path)
- post.set_form_data(data)
+ uri = URI uri
+ if data && data != 'html'
+ unless active.uh || data[:passwd]
+ raise RedditError, 'You must be logged in to make POST requests.'
+ end
+ post = Net::HTTP::Post.new uri.path
+ post.set_form_data data.merge!(:uh => active.uh, :api_type => 'json')
end
- Snooby.wait if @last_request && Time.now - @last_request < 2
+ wait if @last_request && Time.now - @last_request < @config['delay']
@last_request = Time.now
- Conn.request(uri, post).body
+
+ resp = Conn.request uri, post
+ raise ArgumentError, resp.code_type unless resp.code == '200'
+
+ # Raw HTML is parsed to obtain the user's trophy data and karma breakdown.
+ return resp.body if data == 'html'
+
+ json = JSON.parse resp.body, :max_nesting => 100
+ if (resp = json['json']) && (errors = resp['errors'])
+ raise RedditError, jj(json) unless errors.empty?
+ end
+ json
end
# The crux of Snooby. Generates an array of structs from the Paths and Fields
@@ -67,7 +129,7 @@ def self.request(uri, data = nil)
# object (say, Snooby::Comment) doesn't expose which kind of comment it is.
def self.build(object, path, which, count)
# A bit of string manipulation to determine which fields to populate the
- # generated struct with. There might be a less fragile way to go about it,
+ # generated structs with. There might be a less fragile way to go about it,
# but it shouldn't be a problem as long as naming remains consistent.
kind = object.to_s.split('::')[1].downcase.to_sym
@@ -76,10 +138,9 @@ def self.build(object, path, which, count)
# an empty result set that the generated structs will be pushed into.
limit, after, results = [count, 100].min, '', []
- # Fetch data until we've met the count or reached the end of the results.
while results.size < count && after
uri = Paths[path] % which + "?limit=#{limit}&after=#{after}"
- json = JSON.parse(Snooby.request(uri), :max_nesting => 100)
+ json = request uri
json = json[1] if path == :post_comments # skip over the post's data
json['data']['children'].each do |child|
# Converts each child's JSON data into the relevant struct based on the
@@ -97,28 +158,15 @@ def self.build(object, path, which, count)
results
end
- class << self
- attr_accessor :config, :active
- end
-
- # Used for permanent storage of preferences and authorization data.
- # Each client should have its own directory to prevent pollution.
- @config = JSON.parse(File.read('.snooby')) rescue {'auth' => {}}
-
# Called whenever respecting the API is required.
def self.wait
- sleep 2
+ sleep @config['delay']
end
-
- # Raised with a pretty print of the relevant JSON object whenever an API call
- # returns a non-empty "errors" array, typically in cases of rate limiting and
- # missing or inaccurate authorization.
- class RedditError < StandardError; end
end
# Snooby's parts are required down here, after its initial declaration, because
# Post and Comment are structs whose definitions are taken from the Fields hash
-# above, and related bits might as well be kept together.
-%w[client actions user subreddit post comment].each do |d|
- require "snooby/#{d}"
+# and related bits might as well be kept together.
+%w[client actions user subreddit domain post comment].each do |dep|
+ require "snooby/#{dep}"
end
View
@@ -1,78 +1,72 @@
module Snooby
+
module About
# Returns a hash containing the calling object's about.json data.
def about
- uri = URI(Paths[:"#{@kind}_about"] % @name)
- JSON.parse(Snooby.request(uri))['data']
+ Snooby.request(Paths[:"#{@kind}_about"] % @name)['data']
end
end
module Posts
# Returns an array of structs containing the calling object's posts.
def posts(count = 25)
path = @name ? :"#{@kind}_posts" : :reddit
- Snooby.build(Post, path, @name, count)
+ Snooby.build Post, path, @name, count
end
+ alias :submissions :posts
end
module Comments
# Returns an array of structs containing the calling object's comments.
# TODO: return more than just top-level comments for posts.
- def comments(count = 25)
+ def comments(count = @kind == 'post' ? 500 : 25)
# @name suffices for users and subreddits, but a post's name is obtained
# from its struct; the "t3_" must be removed before making the API call.
@name ||= self.name[3..-1]
- Snooby.build(Comment, :"#{@kind}_comments", @name, count)
+ Snooby.build Comment, :"#{@kind}_comments", @name, count
end
end
module Reply
# Posts a reply to the calling object, which is either a post or a comment.
def reply(text)
- raise RedditError, 'You are not logged in.' unless Snooby.active
-
- data = {:parent => self.name, :text => text, :api_type => 'json'}
- resp = Snooby.request(Paths[:comment], data)
- json = JSON.parse(resp)['json']
-
- raise RedditError, jj(json) unless json['errors'].empty?
+ Snooby.request Paths[:comment], :parent => self.name, :text => text
end
end
module Delete
- # Deletes the calling object, which is either a post or a comment, as long
- # as it belongs to the currently authorized user.
+ # Deletes the calling object, which is either a post or a comment.
def delete
- raise RedditError, 'You are not logged in.' unless Snooby.active
- unless self.author == Snooby.active.username
- #raise CommonDecencyError
- raise RedditError, "You can't delete somebody else's content."
- end
-
- Snooby.request(Paths[:delete], :id => self.name)
+ Snooby.request Paths[:delete], :id => self.name
end
end
module Compose
# Sends a message to the calling object, which is either a subreddit or a
# user; in the case of the former, this behaves like moderator mail.
def compose(subject, text)
- raise RedditError, 'You are not logged in.' unless Snooby.active
-
to = (@kind == 'user' ? '' : '#') + @name
data = {:to => to, :subject => subject, :text => text}
- Snooby.request(Paths[:compose], data)
+ Snooby.request Paths[:compose], data
end
alias :message :compose
end
module Voting
def vote(dir)
- Snooby.request(Paths[:vote], :id => self.name, :dir => dir)
+ Snooby.request Paths[:vote], :id => self.name, :dir => dir
end
- def upvote; vote 1; end
- def rescind; vote 0; end
- def downvote; vote -1; end
+ def upvote
+ vote 1
+ end
+
+ def rescind
+ vote 0
+ end
+
+ def downvote
+ vote -1
+ end
end
end
Oops, something went wrong.

0 comments on commit 087963b

Please sign in to comment.