snooby 0.1.0 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
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