SnoobyPlus 0.1
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/lib/snooby.rb +178 -0
- data/lib/snooby/actions.rb +73 -0
- data/lib/snooby/client.rb +105 -0
- data/lib/snooby/comment.rb +11 -0
- data/lib/snooby/domain.rb +11 -0
- data/lib/snooby/post.rb +35 -0
- data/lib/snooby/user.rb +48 -0
- metadata +71 -0
data/lib/snooby.rb
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
%w[json net/http/persistent].each { |dep| require dep }
|
2
|
+
|
3
|
+
CONFIG_FILE = '.snooby/config.json'
|
4
|
+
|
5
|
+
# Creates and initializes configuration and caching on a per-directory basis.
|
6
|
+
# Doesn't update if they already exist, but this might need to be fleshed out
|
7
|
+
# a bit in future to merge values to be kept with new defaults in upgrades.
|
8
|
+
unless File.exists? CONFIG_FILE
|
9
|
+
DEFAULT_CONFIG = File.join Gem.datadir('snooby'), 'config.json'
|
10
|
+
%w[.snooby .snooby/cache].each { |dir| Dir.mkdir dir }
|
11
|
+
File.open(CONFIG_FILE, 'w') { |file| file << File.read(DEFAULT_CONFIG) }
|
12
|
+
end
|
13
|
+
|
14
|
+
# reddit API (Snoo) + happy programming (Ruby) = Snooby
|
15
|
+
module Snooby
|
16
|
+
|
17
|
+
class << self
|
18
|
+
attr_accessor :config, :active
|
19
|
+
end
|
20
|
+
|
21
|
+
# Raised with a pretty print of the relevant JSON object whenever an API call
|
22
|
+
# returns a non-empty "errors" array, typically in cases of rate limiting and
|
23
|
+
# missing or insufficient authorization. Also used as a general container for
|
24
|
+
# miscellaneous errors related to site functionality.
|
25
|
+
class RedditError < StandardError
|
26
|
+
end
|
27
|
+
|
28
|
+
# Changes to configuration should persist across multiple uses. This class is
|
29
|
+
# a simple modification to the standard hash's setter that updates the config
|
30
|
+
# file whenever a value changes.
|
31
|
+
class Config < Hash
|
32
|
+
def []=(key, value)
|
33
|
+
super
|
34
|
+
return unless Snooby.config # nil during initialization inside JSON#parse
|
35
|
+
File.open(CONFIG_FILE, 'w') do |file|
|
36
|
+
file << JSON.pretty_generate(Snooby.config)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
@config = JSON.parse(File.read(CONFIG_FILE), object_class: Config)
|
42
|
+
raise RedditError, 'Insufficiently patient delay.' if @config['delay'] < 2
|
43
|
+
|
44
|
+
# Opens a persistent connection that provides a significant speed improvement
|
45
|
+
# during repeated calls; reddit's two-second rule pretty much nullifies this,
|
46
|
+
# but it's still a great library and persistent connections are a Good Thing.
|
47
|
+
Conn = Net::HTTP::Persistent.new 'Snooby'
|
48
|
+
|
49
|
+
paths = {
|
50
|
+
:comment => 'api/comment',
|
51
|
+
:compose => 'api/compose',
|
52
|
+
:delete => 'api/del',
|
53
|
+
:disliked => 'user/%s/disliked.json',
|
54
|
+
:domain_posts => 'domain/%s.json',
|
55
|
+
:friend => 'api/friend',
|
56
|
+
:hidden => 'user/%s/hidden.json',
|
57
|
+
:hide => 'api/hide',
|
58
|
+
:liked => 'user/%s/liked.json',
|
59
|
+
:login => 'api/login/%s',
|
60
|
+
:mark => 'api/marknsfw',
|
61
|
+
:me => 'api/me.json',
|
62
|
+
:post_comments => 'comments/%s.json',
|
63
|
+
:reddit => '.json',
|
64
|
+
:save => 'api/save',
|
65
|
+
:saved => 'saved.json',
|
66
|
+
:submit => 'api/submit',
|
67
|
+
:subreddit_about => 'r/%s/about.json',
|
68
|
+
:subreddit_comments => 'r/%s/comments.json',
|
69
|
+
:subreddit_posts => 'r/%s.json',
|
70
|
+
# Snooby-Plus adds Top/Controversial/New grabbing to Snooby
|
71
|
+
:subreddit_top => 'r/%s/top.json',
|
72
|
+
:subreddit_new => 'r/%s/new.json',
|
73
|
+
:subreddit_rising => 'r/%s/rising.json',
|
74
|
+
:subreddit_contro => 'r/%s/controversial.json',
|
75
|
+
# End Snooby-Plus additions
|
76
|
+
:subscribe => 'api/subscribe',
|
77
|
+
:unfriend => 'api/unfriend',
|
78
|
+
:unhide => 'api/unhide',
|
79
|
+
:unmark => 'api/unmarknsfw',
|
80
|
+
:unsave => 'api/unsave',
|
81
|
+
:user => 'user/%s',
|
82
|
+
:user_about => 'user/%s/about.json',
|
83
|
+
:user_comments => 'user/%s/comments.json',
|
84
|
+
:user_posts => 'user/%s/submitted.json',
|
85
|
+
:vote => 'api/vote',
|
86
|
+
}
|
87
|
+
|
88
|
+
# Provides a mapping of things and actions to their respective URL fragments.
|
89
|
+
Paths = paths.merge(paths) { |act, path| "http://www.reddit.com/#{path}" }
|
90
|
+
|
91
|
+
# Provides a mapping of things to a list of all the attributes present in the
|
92
|
+
# relevant JSON object. A lot of these probably won't get used too often, but
|
93
|
+
# might as well grab all available data (except body_html and selftext_html).
|
94
|
+
Fields = {
|
95
|
+
: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],
|
96
|
+
: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]
|
97
|
+
}
|
98
|
+
|
99
|
+
# Wraps the connection for all requests to ensure that the two-second rule is
|
100
|
+
# adhered to. The uri parameter comes in as a string because it may have been
|
101
|
+
# susceptible to variables, so it gets turned into an actual URI here instead
|
102
|
+
# of all over the place. Since it's required for all POST requests other than
|
103
|
+
# logging in, the client's modhash is sent along by default.
|
104
|
+
def self.request(uri, data = nil)
|
105
|
+
uri = URI uri
|
106
|
+
if data && data != 'html'
|
107
|
+
unless active.uh || data[:passwd]
|
108
|
+
raise RedditError, 'You must be logged in to make POST requests.'
|
109
|
+
end
|
110
|
+
post = Net::HTTP::Post.new uri.path
|
111
|
+
post.set_form_data data.merge!(:uh => active.uh, :api_type => 'json')
|
112
|
+
end
|
113
|
+
wait if @last_request && Time.now - @last_request < @config['delay']
|
114
|
+
@last_request = Time.now
|
115
|
+
|
116
|
+
resp = Conn.request uri, post
|
117
|
+
raise ArgumentError, resp.code_type unless resp.code == '200'
|
118
|
+
|
119
|
+
# Raw HTML is parsed to obtain the user's trophy data and karma breakdown.
|
120
|
+
return resp.body if data == 'html'
|
121
|
+
|
122
|
+
json = JSON.parse resp.body, :max_nesting => 100
|
123
|
+
if (resp = json['json']) && (errors = resp['errors'])
|
124
|
+
raise RedditError, jj(json) unless errors.empty?
|
125
|
+
end
|
126
|
+
json
|
127
|
+
end
|
128
|
+
|
129
|
+
# The crux of Snooby. Generates an array of structs from the Paths and Fields
|
130
|
+
# hashes defined above. In addition to just being a very neat container, this
|
131
|
+
# allows accessing the returned JSON values using thing.attribute, as opposed
|
132
|
+
# to thing['data']['attribute']. Only used for listings of posts and comments
|
133
|
+
# at the moment, but I imagine it'll be used for moderation down the road.
|
134
|
+
# Having to explicitly pass the path isn't very DRY, but deriving it from the
|
135
|
+
# object (say, Snooby::Comment) doesn't expose which kind of comment it is.
|
136
|
+
def self.build(object, path, which, count)
|
137
|
+
# A bit of string manipulation to determine which fields to populate the
|
138
|
+
# generated structs with. There might be a less fragile way to go about it,
|
139
|
+
# but it shouldn't be a problem as long as naming remains consistent.
|
140
|
+
kind = object.to_s.split('::')[1].downcase.to_sym
|
141
|
+
|
142
|
+
# Set limit to the maximum of 100 if we're grabbing more than that, give
|
143
|
+
# after a truthy value since we stop when it's no longer so, and initialize
|
144
|
+
# an empty result set that the generated structs will be pushed into.
|
145
|
+
limit, after, results = [count, 100].min, '', []
|
146
|
+
|
147
|
+
while results.size < count && after
|
148
|
+
uri = Paths[path] % which + "?limit=#{limit}&after=#{after}"
|
149
|
+
json = request uri
|
150
|
+
json = json[1] if path == :post_comments # skip over the post's data
|
151
|
+
json['data']['children'].each do |child|
|
152
|
+
# Converts each child's JSON data into the relevant struct based on the
|
153
|
+
# kind of object being built. The symbols of a struct definition are
|
154
|
+
# ordered, but Python dictionaries are not, so #values is insufficient.
|
155
|
+
# Preliminary testing showed that appending one at a time is slightly
|
156
|
+
# faster than concatenating the entire page of results and then taking
|
157
|
+
# a slice at the end. This also allows for premature stopping if the
|
158
|
+
# count is reached before all results have been processed.
|
159
|
+
results << object.new(*child['data'].values_at(*Fields[kind]))
|
160
|
+
return results if results.size == count
|
161
|
+
end
|
162
|
+
after = json['data']['after']
|
163
|
+
end
|
164
|
+
results
|
165
|
+
end
|
166
|
+
|
167
|
+
# Called whenever respecting the API is required.
|
168
|
+
def self.wait
|
169
|
+
sleep @config['delay']
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Snooby's parts are required down here, after its initial declaration, because
|
174
|
+
# Post and Comment are structs whose definitions are taken from the Fields hash
|
175
|
+
# and related bits might as well be kept together.
|
176
|
+
%w[client actions user subreddit domain post comment].each do |dep|
|
177
|
+
require "snooby/#{dep}"
|
178
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Snooby
|
2
|
+
|
3
|
+
module About
|
4
|
+
# Returns a hash containing the calling object's about.json data.
|
5
|
+
def about
|
6
|
+
Snooby.request(Paths[:"#{@kind}_about"] % @name)['data']
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
module Posts
|
11
|
+
# Returns an array of structs containing the calling object's posts.
|
12
|
+
# Snooby-Plus adds the `srlist' parameter. This can be any one of `:top', `:new', `:rising', or `:contro'.
|
13
|
+
def posts(count = 25, srlist = :posts)
|
14
|
+
path = @name ? :"#{@kind}_#{@kind == :subreddit ? srlist : :posts}" : :reddit
|
15
|
+
Snooby.build Post, path, @name, count
|
16
|
+
end
|
17
|
+
alias :submissions :posts
|
18
|
+
end
|
19
|
+
|
20
|
+
module Comments
|
21
|
+
# Returns an array of structs containing the calling object's comments.
|
22
|
+
# TODO: return more than just top-level comments for posts.
|
23
|
+
def comments(count = @kind == 'post' ? 500 : 25)
|
24
|
+
# @name suffices for users and subreddits, but a post's name is obtained
|
25
|
+
# from its struct; the "t3_" must be removed before making the API call.
|
26
|
+
@name ||= self.name[3..-1]
|
27
|
+
Snooby.build Comment, :"#{@kind}_comments", @name, count
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module Reply
|
32
|
+
# Posts a reply to the calling object, which is either a post or a comment.
|
33
|
+
def reply(text)
|
34
|
+
Snooby.request Paths[:comment], :parent => self.name, :text => text
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
module Delete
|
39
|
+
# Deletes the calling object, which is either a post or a comment.
|
40
|
+
def delete
|
41
|
+
Snooby.request Paths[:delete], :id => self.name
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
module Compose
|
46
|
+
# Sends a message to the calling object, which is either a subreddit or a
|
47
|
+
# user; in the case of the former, this behaves like moderator mail.
|
48
|
+
def compose(subject, text)
|
49
|
+
to = (@kind == 'user' ? '' : '#') + @name
|
50
|
+
data = {:to => to, :subject => subject, :text => text}
|
51
|
+
Snooby.request Paths[:compose], data
|
52
|
+
end
|
53
|
+
alias :message :compose
|
54
|
+
end
|
55
|
+
|
56
|
+
module Voting
|
57
|
+
def vote(dir)
|
58
|
+
Snooby.request Paths[:vote], :id => self.name, :dir => dir
|
59
|
+
end
|
60
|
+
|
61
|
+
def upvote
|
62
|
+
vote 1
|
63
|
+
end
|
64
|
+
|
65
|
+
def rescind
|
66
|
+
vote 0
|
67
|
+
end
|
68
|
+
|
69
|
+
def downvote
|
70
|
+
vote -1
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module Snooby
|
2
|
+
|
3
|
+
# Interface through which Snooby does all of its interacting with the API.
|
4
|
+
class Client
|
5
|
+
attr_reader :uh, :id
|
6
|
+
|
7
|
+
def initialize(user_agent = "Snooby, #{rand}")
|
8
|
+
# Net::HTTP's default User-Agent, "Ruby", is banned on reddit due to its
|
9
|
+
# frequent improper use; cheers to Eric Hodel (drbrain) for implementing
|
10
|
+
# this workaround for net-http-persistent.
|
11
|
+
Conn.override_headers['User-Agent'] = user_agent
|
12
|
+
|
13
|
+
# Allows Snooby's classes to access the currently active client, even if
|
14
|
+
# it has not been authorized, allowing the user to log in mid-execution.
|
15
|
+
Snooby.active = self
|
16
|
+
end
|
17
|
+
|
18
|
+
# Causes the client to be recognized as the given user during API calls.
|
19
|
+
# GET operations do not need to be authorized, so if your intent is simply
|
20
|
+
# to gather data, feel free to disregard this method entirely.
|
21
|
+
def authorize!(user, passwd, force_update = false)
|
22
|
+
if Snooby.config['auth'][user] && !force_update
|
23
|
+
# Authorization data exists, skip login and potential rate-limiting.
|
24
|
+
@uh, @cookie, @id = Snooby.config['auth'][user]
|
25
|
+
else
|
26
|
+
data = {:user => user, :passwd => passwd}
|
27
|
+
json = Snooby.request(Paths[:login] % user, data)['json']
|
28
|
+
@uh, @cookie = json['data'].values
|
29
|
+
end
|
30
|
+
|
31
|
+
# Sets the reddit_session cookie required for API calls to be recognized
|
32
|
+
# as coming from the intended user. Uses override_headers to allow for
|
33
|
+
# switching the current user mid-execution, if so desired.
|
34
|
+
Conn.override_headers['Cookie'] = "reddit_session=#{@cookie}"
|
35
|
+
|
36
|
+
# A second call is made, if required, to grab the client's id, which is
|
37
|
+
# necessary for (un)friending.
|
38
|
+
@id ||= "t2_#{me['id']}"
|
39
|
+
|
40
|
+
# Updates the config file to faciliate one-time authorization. This works
|
41
|
+
# because the authorization data is immortal unless the password has been
|
42
|
+
# changed; enable the force_update parameter if such is the case.
|
43
|
+
Snooby.config['auth'][user] = [@uh, @cookie, @id]
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns a User object through which all relevant data is accessed.
|
47
|
+
def user(name)
|
48
|
+
User.new name
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns a Subreddit object through which all relevant data is accessed.
|
52
|
+
# Supplying no name provides access to the client's front page.
|
53
|
+
def subreddit(name = nil)
|
54
|
+
Subreddit.new name
|
55
|
+
end
|
56
|
+
|
57
|
+
def domain(name)
|
58
|
+
Domain.new name
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns a hash containing the values given by me.json, used internally to
|
62
|
+
# obtain the client's id, but also the most efficient way to check whether
|
63
|
+
# or not the client has mail.
|
64
|
+
def me
|
65
|
+
Snooby.request(Paths[:me])['data']
|
66
|
+
end
|
67
|
+
|
68
|
+
# Returns an array of structs containing the current client's saved posts.
|
69
|
+
def saved(count = 25)
|
70
|
+
Snooby.build Post, :saved, nil, count
|
71
|
+
end
|
72
|
+
|
73
|
+
def submit(name, title, content)
|
74
|
+
Subreddit.new(name).submit title, content
|
75
|
+
end
|
76
|
+
|
77
|
+
def subscribe(name)
|
78
|
+
Subreddit.new(name).subscribe
|
79
|
+
end
|
80
|
+
|
81
|
+
def unsubscribe(name)
|
82
|
+
Subreddit.new(name).unsubscribe
|
83
|
+
end
|
84
|
+
|
85
|
+
def friend(name)
|
86
|
+
User.new(name).friend
|
87
|
+
end
|
88
|
+
|
89
|
+
def unfriend(name)
|
90
|
+
User.new(name).unfriend
|
91
|
+
end
|
92
|
+
|
93
|
+
def compose(to, subject, text)
|
94
|
+
data = {:to => to, :subject => subject, :text => text}
|
95
|
+
Snooby.request Paths[:compose], data
|
96
|
+
end
|
97
|
+
|
98
|
+
# Aliases all in one place for purely aesthetic reasons.
|
99
|
+
alias :u :user
|
100
|
+
alias :r :subreddit
|
101
|
+
alias :sub :subscribe
|
102
|
+
alias :unsub :unsubscribe
|
103
|
+
alias :message :compose
|
104
|
+
end
|
105
|
+
end
|
data/lib/snooby/post.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
module Snooby
|
2
|
+
|
3
|
+
class Post < Struct.new(*Fields[:post].map(&:to_sym))
|
4
|
+
include Comments, Reply, Delete, Voting
|
5
|
+
|
6
|
+
def initialize(*)
|
7
|
+
super
|
8
|
+
@kind = 'post'
|
9
|
+
end
|
10
|
+
|
11
|
+
def save(un = '')
|
12
|
+
Snooby.request Paths[:"#{un}save"], :id => self.name
|
13
|
+
end
|
14
|
+
|
15
|
+
def unsave
|
16
|
+
save 'un'
|
17
|
+
end
|
18
|
+
|
19
|
+
def hide(un = '')
|
20
|
+
Snooby.request Paths[:"#{un}hide"], :id => self.name
|
21
|
+
end
|
22
|
+
|
23
|
+
def unhide
|
24
|
+
hide 'un'
|
25
|
+
end
|
26
|
+
|
27
|
+
def mark(un = '')
|
28
|
+
Snooby.request Paths[:"#{un}mark"], :id => self.name
|
29
|
+
end
|
30
|
+
|
31
|
+
def unmark
|
32
|
+
mark 'un'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/snooby/user.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
module Snooby
|
2
|
+
|
3
|
+
class User
|
4
|
+
include About, Posts, Comments, Compose
|
5
|
+
|
6
|
+
def initialize(name)
|
7
|
+
@name = name
|
8
|
+
@kind = 'user'
|
9
|
+
end
|
10
|
+
|
11
|
+
# Returns an array of arrays containing the user's trophy information in
|
12
|
+
# the form of [name, description], the latter containing the empty string
|
13
|
+
# if inapplicable.
|
14
|
+
def trophies
|
15
|
+
# Only interested in trophies; request the minimum amount of content.
|
16
|
+
html = Snooby.request(Paths[:user] % @name + '?limit=1', 'html')
|
17
|
+
# Entry-level black magic.
|
18
|
+
html.scan /"trophy-name">(.+?)<.+?"\s?>([^<]*)</
|
19
|
+
end
|
20
|
+
|
21
|
+
def karma_breakdown
|
22
|
+
html = Snooby.request(Paths[:user] % @name + '?limit=1', 'html')
|
23
|
+
rx = /h>(.+?)<.+?(\d+).+?(\d+)/
|
24
|
+
Hash[html.split('y>')[2].scan(rx).map { |r| [r.shift, r.map(&:to_i)] }]
|
25
|
+
end
|
26
|
+
|
27
|
+
def liked(count = 25)
|
28
|
+
Snooby.build Post, :liked, @name, count
|
29
|
+
end
|
30
|
+
|
31
|
+
def disliked(count = 25)
|
32
|
+
Snooby.build Post, :disliked, @name, count
|
33
|
+
end
|
34
|
+
|
35
|
+
def hidden(count = 25)
|
36
|
+
Snooby.build Post, :hidden, @name, count
|
37
|
+
end
|
38
|
+
|
39
|
+
def friend(un = '')
|
40
|
+
data = {:name => @name, :type => 'friend', :container => Snooby.active.id}
|
41
|
+
Snooby.request Paths[:"#{un}friend"], data
|
42
|
+
end
|
43
|
+
|
44
|
+
def unfriend
|
45
|
+
friend 'un'
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: SnoobyPlus
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 9
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: "0.1"
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- King Bowser
|
13
|
+
- andkerosine
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2013-08-05 00:00:00 Z
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: All the features of Snooby, along with TopRisingNewControversial list grabbing.
|
22
|
+
email: backseat.rapist@gmail.com
|
23
|
+
executables: []
|
24
|
+
|
25
|
+
extensions: []
|
26
|
+
|
27
|
+
extra_rdoc_files: []
|
28
|
+
|
29
|
+
files:
|
30
|
+
- lib/snooby.rb
|
31
|
+
- lib/snooby/actions.rb
|
32
|
+
- lib/snooby/client.rb
|
33
|
+
- lib/snooby/comment.rb
|
34
|
+
- lib/snooby/domain.rb
|
35
|
+
- lib/snooby/post.rb
|
36
|
+
- lib/snooby/user.rb
|
37
|
+
homepage: https://github.com/KingBowser/snooby
|
38
|
+
licenses: []
|
39
|
+
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options: []
|
42
|
+
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
none: false
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
hash: 3
|
51
|
+
segments:
|
52
|
+
- 0
|
53
|
+
version: "0"
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
hash: 3
|
60
|
+
segments:
|
61
|
+
- 0
|
62
|
+
version: "0"
|
63
|
+
requirements: []
|
64
|
+
|
65
|
+
rubyforge_project:
|
66
|
+
rubygems_version: 1.8.15
|
67
|
+
signing_key:
|
68
|
+
specification_version: 3
|
69
|
+
summary: All the features of Snooby, along with TopRisingNewControversial list grabbing.
|
70
|
+
test_files: []
|
71
|
+
|