snooby 0.1.0 → 0.1.5

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.
data/LICENSE ADDED
@@ -0,0 +1,14 @@
1
+ Copyright (C) 2012 Donnie Akers
2
+
3
+ This program is free software: you can redistribute it and/or modify
4
+ it under the terms of the GNU General Public License as published by
5
+ the Free Software Foundation, either version 3 of the License, or
6
+ (at your option) any later version.
7
+
8
+ This program is distributed in the hope that it will be useful,
9
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ GNU General Public License for more details.
12
+
13
+ You should have received a copy of the GNU General Public License
14
+ along with this program. If not, see http://www.gnu.org/licenses.
data/README.md CHANGED
@@ -4,7 +4,7 @@ Snooby is a wrapper around the reddit API written in Ruby. It aims to make autom
4
4
  ## Install
5
5
  gem install snooby
6
6
 
7
- ## Example
7
+ ## Examples
8
8
 
9
9
  Here's one way you might go about implementing a very simple bot that constantly monitors new comments to scold users of crass language.
10
10
 
@@ -13,30 +13,42 @@ require 'snooby'
13
13
 
14
14
  probot = Snooby::Client.new('ProfanityBot, v1.0')
15
15
  probot.authorize!('ProfanityBot', 'hunter2')
16
+
16
17
  while true
17
- new_comments = probot.r('all').comments
18
- sleep 2 # Respecting the API is currently manual, will be fixed in future.
19
- new_comments.each do |com|
18
+ probot.r('all').comments.each do |com|
20
19
  if com.body =~ /(vile|rotten|words)/
21
20
  com.reply("#{$&.capitalize} is a terrible word, #{com.author}!")
22
- sleep 2
23
21
  end
24
22
  end
25
- sleep 2
26
23
  end
27
24
  ```
28
- ## Features
29
25
 
30
- Snooby is in the early stages of active development. Most of the code is structure, but there is *some* functionality in place. At the moment, Snooby can:
26
+ That covers most of the core features, but here's a look at a few more in closer detail.
27
+
28
+ ```ruby
29
+ reddit = Snooby::Client.new
30
+
31
+ reddit.user('andrewsmith1986').about['comment_karma'] # => 548027
32
+ reddit.u('violentacrez').trophies.size # => 46
33
+
34
+ reddit.subreddit('askscience').posts[0].selftext # => We see lots of people...
35
+ reddit.r('pics').message('Ban imgur.', "Wouldn't that be lulzy?")
36
+
37
+ frontpage = reddit.r # note the lack of parameters
38
+ frontpage.posts[-1].reply('Welcome to the front page.')
39
+
40
+ # Downvote everything I've ever said. (Note: most of your votes won't count.)
41
+ reddit.u('HazierPhonics').comments(1000).each { |c| c.downvote }
42
+
43
+ # Similarly, upvote everything on the front page of /r/askscience. (Same disclaimer.)
44
+ reddit.r('askscience').posts.each { |p| p.upvote }
45
+ ```
31
46
 
32
- * grab the first page of comments/posts for a user/subreddit
33
- * grab about data for users and subreddits
34
- * grab trophy data
35
- * reply to comments and posts
47
+ The code is thoroughly documented and is the best place to start with questions.
36
48
 
37
49
  ## TODO
38
50
 
39
- * Pagination
40
- * Flesh out errors
51
+ * Moderation
41
52
  * Much more thorough configuration file
42
- * Granular caching
53
+ * Granular caching
54
+ * Elegant solution to the "more comments" problem
@@ -2,79 +2,123 @@
2
2
 
3
3
  module Snooby
4
4
  # Opens a persistent connection that provides a significant speed improvement
5
- # during repeated calls; reddit's rate limit nullifies this for the most part,
6
- # but it's still a nice library and persistent connections are a Good Thing.
5
+ # during repeated calls; reddit's two-second rule pretty much nullifies this,
6
+ # but it's still a great library and persistent connections are a Good Thing.
7
7
  Conn = Net::HTTP::Persistent.new('snooby')
8
8
 
9
9
  # Provides a mapping of things and actions to their respective URL fragments.
10
10
  # A path is eventually used as a complete URI, thus the merge.
11
11
  paths = {
12
12
  :comment => 'api/comment',
13
+ :compose => 'api/compose',
14
+ :delete => 'api/del',
15
+ :disliked => 'user/%s/disliked.json',
16
+ :friend => 'api/friend',
17
+ :hidden => 'user/%s/hidden.json',
18
+ :liked => 'user/%s/liked.json',
13
19
  :login => 'api/login/%s',
14
20
  :me => 'api/me.json',
21
+ :post_comments => 'comments/%s.json',
22
+ :reddit => '.json',
23
+ :saved => 'saved.json',
15
24
  :subreddit_about => 'r/%s/about.json',
16
25
  :subreddit_comments => 'r/%s/comments.json',
17
26
  :subreddit_posts => 'r/%s.json',
27
+ :subscribe => 'api/subscribe',
28
+ :unfriend => 'api/unfriend',
18
29
  :user => 'user/%s',
19
30
  :user_about => 'user/%s/about.json',
20
31
  :user_comments => 'user/%s/comments.json',
21
32
  :user_posts => 'user/%s/submitted.json',
33
+ :vote => 'api/vote'
22
34
  }
23
- Paths = paths.merge(paths) { |k, v| 'http://www.reddit.com/' + v }
35
+ Paths = paths.merge(paths) { |k, v| "http://www.reddit.com/#{v}" }
24
36
 
25
37
  # Provides a mapping of things to a list of all the attributes present in the
26
38
  # relevant JSON object. A lot of these probably won't get used too often, but
27
- # might as well expose all available data.
39
+ # might as well grab all available data (except body_html and selftext_html).
28
40
  Fields = {
29
- :comment => %w[author author_flair_css_class author_flair_text body body_html created created_utc downs id likes link_id link_title name parent_id replies subreddit subreddit_id ups],
30
- :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 selftext_html subreddit subreddit_id thumbnail title ups url]
41
+ :comment => %w[author author_flair_css_class author_flair_text body created created_utc downs id likes link_id link_title name parent_id replies subreddit subreddit_id ups],
42
+ :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]
31
43
  }
32
44
 
45
+ # Wraps the connection created above for both POST and GET requests to ensure
46
+ # that the two-second rule is adhered to. The uri parameter is turned into an
47
+ # actual URI once here instead of all over the place. The client's modhash is
48
+ # always required for POST requests, so it is passed along by default.
49
+ def self.request(uri, data = nil)
50
+ uri = URI(uri)
51
+ if data
52
+ data.merge!(:uh => Snooby.active.uh) if Snooby.active
53
+ post = Net::HTTP::Post.new(uri.path)
54
+ post.set_form_data(data)
55
+ end
56
+ Snooby.wait if @last_request && Time.now - @last_request < 2
57
+ @last_request = Time.now
58
+ Conn.request(uri, post).body
59
+ end
60
+
33
61
  # The crux of Snooby. Generates an array of structs from the Paths and Fields
34
62
  # hashes defined above. In addition to just being a very neat container, this
35
63
  # allows accessing the returned JSON values using thing.attribute, as opposed
36
64
  # to thing['data']['attribute']. Only used for listings of posts and comments
37
65
  # at the moment, but I imagine it'll be used for moderation down the road.
38
- def self.build(object, path, which)
66
+ # Having to explicitly pass the path isn't very DRY, but deriving it from the
67
+ # object (say, Snooby::Comment) doesn't expose which kind of comment it is.
68
+ def self.build(object, path, which, count)
39
69
  # A bit of string manipulation to determine which fields to populate the
40
70
  # generated struct with. There might be a less fragile way to go about it,
41
71
  # but it shouldn't be a problem as long as naming remains consistent.
42
72
  kind = object.to_s.split('::')[1].downcase.to_sym
43
73
 
44
- # Having to explicitly pass the path symbol isn't exactly DRY, but deriving
45
- # it from the object parameter (say, Snooby::Comment) doesn't expose which
46
- # kind of comment it is, either User or Post.
47
- uri = URI(Paths[path] % which)
74
+ # Set limit to the maximum of 100 if we're grabbing more than that, give
75
+ # after a truthy value since we stop when it's no longer so, and initialize
76
+ # an empty result set that the generated structs will be pushed into.
77
+ limit, after, results = [count, 100].min, '', []
48
78
 
49
- # This'll likely have to be tweaked to handle other types of listings, but
50
- # it's sufficient for comments and posts.
51
- JSON.parse(Conn.request(uri).body)['data']['children'].map do |child|
52
- # Maps each of the listing's children to the relevant struct based on the
53
- # object type passed in. The symbols in a struct definition are ordered,
54
- # but Python dictionaries are not, so #values isn't sufficient.
55
- object.new(*child['data'].values_at(*Fields[kind]))
79
+ # Fetch data until we've met the count or reached the end of the results.
80
+ while results.size < count && after
81
+ uri = Paths[path] % which + "?limit=#{limit}&after=#{after}"
82
+ json = JSON.parse(Snooby.request(uri), :max_nesting => 100)
83
+ json = json[1] if path == :post_comments # skip over the post's data
84
+ json['data']['children'].each do |child|
85
+ # Converts each child's JSON data into the relevant struct based on the
86
+ # kind of object being built. The symbols of a struct definition are
87
+ # ordered, but Python dictionaries are not, so #values is insufficient.
88
+ # Preliminary testing showed that appending one at a time is slightly
89
+ # faster than concatenating the entire page of results and then taking
90
+ # a slice at the end. This also allows for premature stopping if the
91
+ # count is reached before all results have been processed.
92
+ results << object.new(*child['data'].values_at(*Fields[kind]))
93
+ return results if results.size == count
94
+ end
95
+ after = json['data']['after']
56
96
  end
97
+ results
57
98
  end
58
99
 
59
100
  class << self
60
- attr_accessor :config, :auth
101
+ attr_accessor :config, :active
61
102
  end
62
103
 
63
104
  # Used for permanent storage of preferences and authorization data.
64
105
  # Each client should have its own directory to prevent pollution.
65
106
  @config = JSON.parse(File.read('.snooby')) rescue {'auth' => {}}
66
107
 
108
+ # Called whenever respecting the API is required.
109
+ def self.wait
110
+ sleep 2
111
+ end
112
+
67
113
  # Raised with a pretty print of the relevant JSON object whenever an API call
68
- # returns a non-empty "errors" field. Where "pretty" is inapplicable, such as
69
- # when the returned JSON object is a series of jQuery calls, a manual message
70
- # is displayed instead, typically to inform the user either that they've been
71
- # rate-limited or that they lack the necessary authorization.
114
+ # returns a non-empty "errors" array, typically in cases of rate limiting and
115
+ # missing or inaccurate authorization.
72
116
  class RedditError < StandardError; end
73
117
  end
74
118
 
75
119
  # Snooby's parts are required down here, after its initial declaration, because
76
120
  # Post and Comment are structs whose definitions are taken from the Fields hash
77
121
  # above, and related bits might as well be kept together.
78
- %w[client actions objects].each do |d|
122
+ %w[client actions user subreddit post comment].each do |d|
79
123
  require "snooby/#{d}"
80
124
  end
@@ -1,69 +1,78 @@
1
1
  module Snooby
2
- # Mixin to provide functionality to all of the objects in one fell swoop,
3
- # both relevant and irrelevant. Errors are thrown where nonsensical methods
4
- # are called, but hopefully the intent is to use Snooby sensibly. I realize
5
- # this is probably bad design, but it's just so damned clean.
6
- module Actions
7
- # Returns a hash containing the values supplied by about.json, so long as
8
- # the calling object is a User or Subreddit.
2
+ module About
3
+ # Returns a hash containing the calling object's about.json data.
9
4
  def about
10
- if !['user', 'subreddit'].include?(@kind)
11
- raise RedditError, 'Only users and subreddits have about pages.'
12
- end
13
-
14
5
  uri = URI(Paths[:"#{@kind}_about"] % @name)
15
- JSON.parse(Conn.request(uri).body)['data']
6
+ JSON.parse(Snooby.request(uri))['data']
16
7
  end
8
+ end
17
9
 
18
- # Returns an array of structs containing the object's posts.
19
- def posts
20
- if !['user', 'subreddit'].include?(@kind)
21
- raise RedditError, 'Only users and subreddits have posts.'
22
- end
10
+ module Posts
11
+ # Returns an array of structs containing the calling object's posts.
12
+ def posts(count = 25)
13
+ path = @name ? :"#{@kind}_posts" : :reddit
14
+ Snooby.build(Post, path, @name, count)
15
+ end
16
+ end
23
17
 
24
- Snooby.build(Post, :"#{@kind}_posts", @name)
18
+ module Comments
19
+ # Returns an array of structs containing the calling object's comments.
20
+ # TODO: return more than just top-level comments for posts.
21
+ def comments(count = 25)
22
+ # @name suffices for users and subreddits, but a post's name is obtained
23
+ # from its struct; the "t3_" must be removed before making the API call.
24
+ @name ||= self.name[3..-1]
25
+ Snooby.build(Comment, :"#{@kind}_comments", @name, count)
25
26
  end
27
+ end
26
28
 
27
- # Returns an array of structs containing the object's comments.
28
- def comments
29
- if !['user', 'subreddit'].include?(@kind)
30
- raise RedditError, 'Only users and subreddits have comments.'
31
- end
29
+ module Reply
30
+ # Posts a reply to the calling object, which is either a post or a comment.
31
+ def reply(text)
32
+ raise RedditError, 'You are not logged in.' unless Snooby.active
33
+
34
+ data = {:parent => self.name, :text => text, :api_type => 'json'}
35
+ resp = Snooby.request(Paths[:comment], data)
36
+ json = JSON.parse(resp)['json']
32
37
 
33
- Snooby.build(Comment, :"#{@kind}_comments", @name)
38
+ raise RedditError, jj(json) unless json['errors'].empty?
34
39
  end
40
+ end
35
41
 
36
- # Returns an array of 2-tuples containing the user's trophy information in
37
- # the form of [name, description], the latter containing the empty string
38
- # if inapplicable.
39
- def trophies
40
- raise RedditError, 'Only users have trophies.' if @kind != 'user'
42
+ module Delete
43
+ # Deletes the calling object, which is either a post or a comment, as long
44
+ # as it belongs to the currently authorized user.
45
+ def delete
46
+ raise RedditError, 'You are not logged in.' unless Snooby.active
47
+ unless self.author == Snooby.active.username
48
+ #raise CommonDecencyError
49
+ raise RedditError, "You can't delete somebody else's content."
50
+ end
41
51
 
42
- # Only interested in trophies; request the minimum amount of content.
43
- html = Conn.request(URI(Paths[:user] % @name) + '?limit=1').body
44
- # Entry-level black magic.
45
- html.scan(/"trophy-name">(.+?)<.+?"\s?>([^<]*)</)
52
+ Snooby.request(Paths[:delete], :id => self.name)
46
53
  end
54
+ end
47
55
 
48
- # Posts a reply to the caller as the currently authorized user, so long as
49
- # that caller is a Post or Comment.
50
- def reply(text)
51
- raise RedditError, 'You must be authorized to comment.' if !Snooby.auth
56
+ module Compose
57
+ # Sends a message to the calling object, which is either a subreddit or a
58
+ # user; in the case of the former, this behaves like moderator mail.
59
+ def compose(subject, text)
60
+ raise RedditError, 'You are not logged in.' unless Snooby.active
52
61
 
53
- if !['post', 'comment'].include?(@kind)
54
- raise RedditError, "Replying to a #{@kind} doesn't make much sense."
55
- end
56
-
57
- uri = URI(Paths[:comment])
58
- post = Net::HTTP::Post.new(uri.path)
59
- data = {:parent => self.name, :text => text, :uh => Snooby.auth}
60
- post.set_form_data(data)
61
- json = JSON.parse(Conn.request(uri, post).body)['jquery']
62
+ to = (@kind == 'user' ? '' : '#') + @name
63
+ data = {:to => to, :subject => subject, :text => text}
64
+ Snooby.request(Paths[:compose], data)
65
+ end
66
+ alias :message :compose
67
+ end
62
68
 
63
- # Bad magic numbers, I know, but getting rate-limited during a reply
64
- # returns a bunch of serialized jQuery rather than a straightforward
65
- # and meaningful message. Fleshing out errors is on the to-do list.
66
- raise RedditError, json[14][3] if json.size == 17
69
+ module Voting
70
+ def vote(dir)
71
+ Snooby.request(Paths[:vote], :id => self.name, :dir => dir)
67
72
  end
73
+
74
+ def upvote; vote 1; end
75
+ def rescind; vote 0; end
76
+ def downvote; vote -1; end
68
77
  end
69
78
  end
@@ -1,7 +1,12 @@
1
1
  module Snooby
2
2
  # Interface through which Snooby does all of its interacting with the API.
3
3
  class Client
4
- def initialize(user_agent = 'Snooby')
4
+ # Exposes username to raise a proper error in case of an attempt to delete
5
+ # another user's content, modhash (uh) for sending along in the headers on
6
+ # API calls that change state, and id for (un)friending.
7
+ attr_reader :username, :uh, :id
8
+
9
+ def initialize(user_agent = "Snooby, #{rand}")
5
10
  # Net::HTTP's default User-Agent, "Ruby", is banned on reddit due to its
6
11
  # frequent improper use; cheers to Eric Hodel (drbrain) for implementing
7
12
  # this workaround for net-http-persistent.
@@ -12,54 +17,90 @@ module Snooby
12
17
  # GET operations do not need to be authorized, so if your intent is simply
13
18
  # to gather data, feel free to disregard this method entirely.
14
19
  def authorize!(user, passwd, force_update = false)
20
+ @username = user
21
+
15
22
  if Snooby.config['auth'][user] && !force_update
16
23
  # Authorization data exists, skip login and potential rate-limiting.
17
- @modhash, @cookie = Snooby.config['auth'][user]
24
+ @uh, @cookie, @id = Snooby.config['auth'][user]
18
25
  else
19
- uri = URI(Paths[:login] % user)
20
- post = Net::HTTP::Post.new(uri.path)
21
- data = {:user => user, :passwd => CGI.escape(passwd), :api_type => 'json'}
22
- post.set_form_data(data)
23
- json = JSON.parse(Conn.request(uri, post).body)['json']
26
+ data = {:user => user, :passwd => passwd, :api_type => 'json'}
27
+ resp = Snooby.request(Paths[:login] % user, data)
28
+ json = JSON.parse(resp)['json']
24
29
 
25
30
  # Will fire for incorrect login credentials and when rate-limited.
26
- raise RedditError, jj(json) if !json['errors'].empty?
27
-
28
- # Parse authorization data and store it both in the current config, as
29
- # well as in the configuration file for future use. This works because
30
- # authorization data is immortal unless the password has changed. The
31
- # force_update parameter should be enabled if such is the case.
32
- @modhash, @cookie = json['data'].values
33
- Snooby.config['auth'][user] = [@modhash, @cookie]
34
- File.open('.snooby', 'w') { |f| f << Snooby.config.to_json }
31
+ raise RedditError, jj(json) unless json['errors'].empty?
32
+
33
+ # Parse authorization data.
34
+ @uh, @cookie = json['data'].values
35
35
  end
36
36
 
37
37
  # Sets the reddit_session cookie required for API calls to be recognized
38
- # as coming from the intended user; uses override_headers to allow for
38
+ # as coming from the intended user. Uses override_headers to allow for
39
39
  # switching the current user mid-execution, if so desired.
40
- Conn.headers['Cookie'] = "reddit_session=#{@cookie}"
40
+ Conn.override_headers['Cookie'] = "reddit_session=#{@cookie}"
41
41
 
42
- # Allows Snooby's classes to access the currently authorized client.
43
- Snooby.auth = @modhash
44
- end
42
+ # A second call is made, if required, to grab the client's id, which is
43
+ # necessary for (un)friending.
44
+ @id ||= "t2_#{me['id']}"
45
45
 
46
- # Returns a hash containing the values supplied by me.json.
47
- def me
48
- uri = URI(Paths[:me])
49
- JSON.parse(Conn.request(uri).body)['data']
46
+ # Updates the config file to faciliate one-time authorization. This works
47
+ # because the authorization data is immortal unless the password has been
48
+ # changed; enable the force_update parameter if such is the case.
49
+ Snooby.config['auth'][user] = [@uh, @cookie, @id]
50
+ File.open('.snooby', 'w') { |f| f << Snooby.config.to_json }
51
+
52
+ # Allows Snooby's classes to access the currently authorized client.
53
+ Snooby.active = self
50
54
  end
51
55
 
52
- # Returns a User object from which posts, comments, etc. can be accessed.
56
+ # Returns a User object through which all relevant data is accessed.
53
57
  def user(name)
54
58
  User.new(name)
55
59
  end
60
+ alias :u :user
56
61
 
57
- # As above, so below.
58
- def subreddit(name)
62
+ # Returns a Subreddit object through which all relevant data is accessed.
63
+ def subreddit(name = nil)
59
64
  Subreddit.new(name)
60
65
  end
61
-
62
- alias :u :user
63
66
  alias :r :subreddit
67
+
68
+ # Returns a hash containing the values given by me.json, used internally to
69
+ # obtain the client's id, but also the most efficient way to check whether
70
+ # or not the client has mail.
71
+ def me
72
+ JSON.parse(Snooby.request(Paths[:me]))['data']
73
+ end
74
+
75
+ # Returns an array of structs containing the current client's saved posts.
76
+ def saved(count = 25)
77
+ Snooby.build(Post, :saved, nil, count)
78
+ end
79
+
80
+ # Convenience methods.
81
+
82
+ def friend(name)
83
+ User.new(name).friend
84
+ end
85
+
86
+ def unfriend(name)
87
+ User.new(name).unfriend
88
+ end
89
+
90
+ def subscribe(name)
91
+ Subreddit.new(name).subscribe
92
+ end
93
+ alias :sub :subscribe
94
+
95
+ def unsubscribe(name)
96
+ Subreddit.new(name).unsubscribe
97
+ end
98
+ alias :unsub :unsubscribe
99
+
100
+ def compose(to, subject, text)
101
+ data = {:to => to, :subject => subject, :text => text}
102
+ Snooby.request(Paths[:compose], data)
103
+ end
104
+ alias :message :compose
64
105
  end
65
106
  end
@@ -0,0 +1,10 @@
1
+ module Snooby
2
+ class Comment < Struct.new(*Fields[:comment].map(&:to_sym))
3
+ include Reply, Delete, Voting
4
+
5
+ def initialize(*)
6
+ super
7
+ @kind = 'comment'
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module Snooby
2
+ class Post < Struct.new(*Fields[:post].map(&:to_sym))
3
+ include Comments, Reply, Delete, Voting
4
+
5
+ def initialize(*)
6
+ super
7
+ @kind = 'post'
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,26 @@
1
+ module Snooby
2
+ class Subreddit
3
+ include About, Posts, Comments, Compose
4
+
5
+ def initialize(name)
6
+ @name = name
7
+ @kind = 'subreddit'
8
+ end
9
+
10
+ # Alas, (un)subscribing by name alone doesn't work, so a separate call must
11
+ # be made to obtain the subreddit's id, thus the wait. Maybe cache this?
12
+ def subscribe
13
+ sr = about['name']
14
+ Snooby.wait
15
+ Snooby.request(Paths[:subscribe], :action => 'sub', :sr => sr)
16
+ end
17
+ alias :sub :subscribe
18
+
19
+ def unsubscribe
20
+ sr = about['name']
21
+ Snooby.wait
22
+ Snooby.request(Paths[:subscribe], :action => 'unsub', :sr => sr)
23
+ end
24
+ alias :unsub :unsubscribe
25
+ end
26
+ end
@@ -0,0 +1,46 @@
1
+ module Snooby
2
+ class User
3
+ include About, Posts, Comments, Compose
4
+
5
+ def initialize(name)
6
+ @name = name
7
+ @kind = 'user'
8
+ end
9
+
10
+ # Returns an array of 2-tuples containing the user's trophy information in
11
+ # the form of [name, description], the latter containing the empty string
12
+ # if inapplicable.
13
+ def trophies
14
+ # Only interested in trophies; request the minimum amount of content.
15
+ html = Snooby.request(URI(Paths[:user] % @name) + '?limit=1')
16
+ # Entry-level black magic.
17
+ html.scan(/"trophy-name">(.+?)<.+?"\s?>([^<]*)</)
18
+ end
19
+
20
+ def liked(count = 25)
21
+ Snooby.build(Post, :liked, @name, count)
22
+ end
23
+
24
+ def disliked(count = 25)
25
+ Snooby.build(Post, :disliked, @name, count)
26
+ end
27
+
28
+ def hidden(count = 25)
29
+ Snooby.build(Post, :hidden, @name, count)
30
+ end
31
+
32
+ def friend
33
+ raise RedditError, 'You are not logged in.' unless Snooby.active
34
+
35
+ data = {:name => @name, :type => 'friend', :container => Snooby.active.id}
36
+ Snooby.request(Paths[:friend], data)
37
+ end
38
+
39
+ def unfriend
40
+ raise RedditError, 'You are not logged in.' unless Snooby.active
41
+
42
+ data = {:name => @name, :type => 'friend', :container => Snooby.active.id}
43
+ Snooby.request(Paths[:unfriend], data)
44
+ end
45
+ end
46
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: snooby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.5
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-02-15 00:00:00.000000000 Z
12
+ date: 2012-02-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: json
16
- requirement: &79734590 !ruby/object:Gem::Requirement
16
+ requirement: &72574440 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *79734590
24
+ version_requirements: *72574440
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: net-http-persistent
27
- requirement: &79734280 !ruby/object:Gem::Requirement
27
+ requirement: &72574180 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,7 +32,7 @@ dependencies:
32
32
  version: '2.5'
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *79734280
35
+ version_requirements: *72574180
36
36
  description:
37
37
  email:
38
38
  - andkerosine@gmail.com
@@ -41,12 +41,16 @@ extensions: []
41
41
  extra_rdoc_files: []
42
42
  files:
43
43
  - Gemfile
44
+ - LICENSE
44
45
  - README.md
45
46
  - Rakefile
46
47
  - lib/snooby.rb
47
48
  - lib/snooby/actions.rb
48
49
  - lib/snooby/client.rb
49
- - lib/snooby/objects.rb
50
+ - lib/snooby/comment.rb
51
+ - lib/snooby/post.rb
52
+ - lib/snooby/subreddit.rb
53
+ - lib/snooby/user.rb
50
54
  homepage: https://github.com/andkerosine/snooby
51
55
  licenses: []
52
56
  post_install_message:
@@ -1,37 +0,0 @@
1
- module Snooby
2
- class User
3
- include Actions
4
-
5
- def initialize(name)
6
- @name = name
7
- @kind = 'user'
8
- end
9
- end
10
-
11
- class Subreddit
12
- include Actions
13
-
14
- def initialize(name)
15
- @name = name
16
- @kind = 'subreddit'
17
- end
18
- end
19
-
20
- class Post < Struct.new(*Fields[:post].map(&:to_sym))
21
- include Actions
22
-
23
- def initialize(*)
24
- super
25
- @kind = 'post'
26
- end
27
- end
28
-
29
- class Comment < Struct.new(*Fields[:comment].map(&:to_sym))
30
- include Actions
31
-
32
- def initialize(*)
33
- super
34
- @kind = 'comment'
35
- end
36
- end
37
- end