ruby-hackernews 1.0.0

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/README.rdoc ADDED
@@ -0,0 +1,114 @@
1
+
2
+ = ruby-hackernews
3
+
4
+ An API over Hacker News
5
+
6
+ == Requirements
7
+
8
+ mechanize (>= 1.0.0)
9
+ require_all (>= 1.1.0)
10
+
11
+ == Installation
12
+
13
+ gem install ruby-hackernews
14
+
15
+ then, in your script:
16
+
17
+ require 'ruby-hackernews'
18
+
19
+ before using it.
20
+
21
+ == Entries
22
+
23
+ You can get entries on the main page with:
24
+
25
+ Entry.all # returns an array of entries
26
+
27
+ You can provide a number of pages:
28
+
29
+ Entry.all(3) # will return the entries on the first 3 pages
30
+
31
+ There are methods for getting specific entry types:
32
+
33
+ Entry.questions # gets the first page of questions (ask NH)
34
+ Entry.newest # get the first page of new links (new)
35
+ Entry.jobs # get the forst page of job offest (jobs)
36
+
37
+ Each Entry instance has the following data:
38
+
39
+ entry = Entry.all.first #get the top entry on the mainpage
40
+
41
+ entry.number # entry's position on HN
42
+
43
+ entry.link.title # the link's name on HN
44
+ entry.link.href # the actual link
45
+ entry.link.site # the referring's site, if any
46
+
47
+ entry.voting.score # the entry's score on HN
48
+
49
+ entry.user.name # submitter's user name
50
+
51
+ entry.time # elapsed time from submission
52
+
53
+ After you've logged in (see below) you can do the following
54
+
55
+ entry.upvote #votes the entry
56
+ entry.write_comment("mycomment") #adds a comment to the entry
57
+ Entry.submit("mytitle", "myurl") #submit a new link
58
+ Entry.submit("myquestion") #submit a new question (ask HN)
59
+
60
+ == Comments
61
+
62
+ You get an entry's comments with:
63
+
64
+ entry.comments
65
+
66
+ You can also get the newest comments on HN with:
67
+
68
+ Comments.newest
69
+ Comments.newest(3) # get the first 3 pages of new comments
70
+
71
+ Each Comment instance has the following data:
72
+
73
+ comment = Entry.all.first.comments.first # gets the first comment of the first entry on HN's main page
74
+
75
+ comment.text # comment's body
76
+ comment.user.name # poster's user name on HN
77
+ comment.voting.score # comment's score
78
+
79
+ Comments are enumerable and threaded, so you can do like:
80
+
81
+ comment[2][0] # gets the third reply to this comment, then the first reply to the reply
82
+ comment.first # get the first reply to this comment
83
+ comment.select do |c| # gets all the comment replies which text contains "test"
84
+ text ~= /test/
85
+ end
86
+ comment.parent # gets the comment's parent (nil if no parent)
87
+
88
+ Once you're logged in (see below), you can do the following:
89
+
90
+ comment.upvote
91
+ comment.downvote
92
+ comment.reply("mycomment")
93
+
94
+ == Logging in
95
+
96
+ You define a user with:
97
+
98
+ user = User.new("username")
99
+
100
+ Then, you log in with:
101
+
102
+ user.login("password")
103
+
104
+ You can log out with:
105
+
106
+ user.logout
107
+
108
+ You have to log out before logging in with a different user.
109
+
110
+ == TO DO
111
+
112
+ Get user info (submission, comments...)
113
+ Create account
114
+ Change user info/settings
data/Rakefile ADDED
@@ -0,0 +1,34 @@
1
+
2
+ require 'rubygems'
3
+ require 'spec/rake/spectask'
4
+ require 'rake'
5
+ require 'rake/clean'
6
+ require 'rake/gempackagetask'
7
+ require 'rake/rdoctask'
8
+ require 'rake/testtask'
9
+
10
+ spec = Gem::Specification.new do |s|
11
+ s.name = 'ruby-hackernews'
12
+ s.version = '1.0.0'
13
+ s.add_dependency('require_all', '>= 1.1.0')
14
+ s.add_dependency('mechanize', '>= 1.0.0')
15
+ s.has_rdoc = false
16
+ s.homepage = "http://github.com/bolthar/ruby-hackernews"
17
+ s.summary = 'An interface to Hacker News'
18
+ s.description = s.summary
19
+ s.author = 'Andrea Dallera'
20
+ s.email = 'andrea@andreadallera.com'
21
+ s.files = %w(README.rdoc Rakefile) + Dir.glob("{lib}/**/*")
22
+ s.require_path = "lib"
23
+ end
24
+
25
+ Rake::GemPackageTask.new(spec) do |p|
26
+ p.gem_spec = spec
27
+ p.need_tar = true
28
+ p.need_zip = true
29
+ end
30
+
31
+ Spec::Rake::SpecTask.new do |t|
32
+ t.warning = true
33
+ t.spec_files = FileList['spec/**/*.rb']
34
+ end
@@ -0,0 +1,55 @@
1
+
2
+ class Comment
3
+ include Enumerable
4
+
5
+ attr_reader :text
6
+ attr_reader :voting
7
+ attr_reader :user
8
+
9
+ attr_accessor :parent
10
+ attr_reader :children
11
+
12
+ def initialize(text, voting, user_info, reply_link)
13
+ @text = text
14
+ @voting = voting
15
+ @user = user_info
16
+ @reply_link = reply_link
17
+ @children = []
18
+ end
19
+
20
+ def <<(comment)
21
+ comment.parent = self
22
+ @children << comment
23
+ end
24
+
25
+ def each(&block)
26
+ @children.each(&block)
27
+ end
28
+
29
+ def <=>(other_comment)
30
+ return other_comment.voting.score <=> @voting.score
31
+ end
32
+
33
+ def method_missing(method, *args, &block)
34
+ @children.send(method, *args, &block)
35
+ end
36
+
37
+ def self.newest(pages = 1)
38
+ return CommentService.new.get_new_comments(pages)
39
+ end
40
+
41
+ def reply(text)
42
+ return false unless @reply_link
43
+ CommentService.new.write_comment(@reply_link, text)
44
+ return true
45
+ end
46
+
47
+ def upvote
48
+ VotingService.new.vote(@voting.upvote)
49
+ end
50
+
51
+ def downvote
52
+ VotingService.new.vote(@voting.downvote)
53
+ end
54
+
55
+ end
@@ -0,0 +1,12 @@
1
+
2
+ class CommentsInfo
3
+
4
+ attr_accessor :count
5
+ attr_reader :page
6
+
7
+ def initialize(count, page)
8
+ @count = count
9
+ @page = page
10
+ end
11
+
12
+ end
@@ -0,0 +1,57 @@
1
+
2
+ class Entry
3
+
4
+ attr_reader :number
5
+ attr_reader :link
6
+ attr_reader :voting
7
+ attr_reader :user
8
+
9
+ def initialize(number, link, voting, user, comments, time)
10
+ @number = number
11
+ @link = link
12
+ @voting = voting
13
+ @user = user
14
+ @time = time
15
+ @comments_info = comments
16
+ end
17
+
18
+ def comments
19
+ unless @comments
20
+ @comments = CommentService.new.get_comments(@comments_info.page)
21
+ end
22
+ return @comments
23
+ end
24
+
25
+ def self.all(pages = 1)
26
+ return EntryService.new.get_entries(pages)
27
+ end
28
+
29
+ def self.newest(pages = 1)
30
+ return EntryService.new.get_new_entries(pages)
31
+ end
32
+
33
+ def self.questions(pages = 1)
34
+ return EntryService.new.get_questions(pages)
35
+ end
36
+
37
+ def self.jobs(pages = 1)
38
+ return EntryService.new.get_jobs(pages)
39
+ end
40
+
41
+ def time
42
+ return @time.time
43
+ end
44
+
45
+ def write_comment(text)
46
+ return CommentService.new.write_comment(@comments_info.page, text)
47
+ end
48
+
49
+ def self.submit(*args)
50
+ return EntryService.new.submit(*args)
51
+ end
52
+
53
+ def upvote
54
+ return VotingService.new.vote(@voting.upvote)
55
+ end
56
+
57
+ end
@@ -0,0 +1,14 @@
1
+
2
+ class LinkInfo
3
+
4
+ attr_reader :title
5
+ attr_reader :href
6
+ attr_reader :site
7
+
8
+ def initialize(title, href, site)
9
+ @title = title
10
+ @href = href
11
+ @site = site
12
+ end
13
+
14
+ end
@@ -0,0 +1,20 @@
1
+
2
+ class TimeInfo
3
+
4
+ SECOND = 1
5
+ MINUTE = 60 * SECOND
6
+ HOUR = 60 * MINUTE
7
+ DAY = 24 * HOUR
8
+
9
+ def time
10
+ return Time.at(@unit_of_measure * @value)
11
+ end
12
+
13
+ def initialize(value, unit_of_measure)
14
+ descriptor = unit_of_measure[unit_of_measure.length - 1].chr == "s" ? unit_of_measure[0..unit_of_measure.length - 2] : unit_of_measure
15
+ @value = value
16
+ @unit_of_measure = self.class.const_get(descriptor.upcase)
17
+ end
18
+
19
+
20
+ end
@@ -0,0 +1,12 @@
1
+
2
+ class UserInfo
3
+
4
+ attr_reader :name
5
+ attr_reader :page
6
+
7
+ def initialize(name, page)
8
+ @name = name
9
+ @page = page
10
+ end
11
+
12
+ end
@@ -0,0 +1,14 @@
1
+
2
+ class VotingInfo
3
+
4
+ attr_reader :score
5
+ attr_reader :upvote
6
+ attr_reader :downvote
7
+
8
+ def initialize(score, upvote, downvote)
9
+ @score = score
10
+ @upvote = upvote
11
+ @downvote = downvote
12
+ end
13
+
14
+ end
@@ -0,0 +1,18 @@
1
+
2
+ class User
3
+
4
+ attr_reader :name
5
+
6
+ def initialize(name)
7
+ @name = name
8
+ end
9
+
10
+ def login(password)
11
+ return LoginService.new.login(@name, password)
12
+ end
13
+
14
+ def logout
15
+ return LoginService.new.logout
16
+ end
17
+
18
+ end
@@ -0,0 +1,62 @@
1
+
2
+ class CommentService
3
+ include MechanizeContext
4
+
5
+ def get_comments(page_url)
6
+ comments = []
7
+ last = comments
8
+ current_level = -1
9
+ page = agent.get(page_url)
10
+ page.search("//table")[3].search("table/tr").select do |tr|
11
+ tr.search("span.comment").inner_html != "[deleted]"
12
+ end.each do |tr|
13
+ comment = parse_comment(tr)
14
+ level = tr.search("img[@src='http://ycombinator.com/images/s.gif']").first['width'].to_i / 40
15
+ difference = current_level - level
16
+ target = last
17
+ (difference + 1).times do
18
+ target = target.parent || comments
19
+ end
20
+ target << comment
21
+ last = comment
22
+ current_level = level
23
+ end
24
+ return comments
25
+ end
26
+
27
+ def get_new_comments(pages = 1)
28
+ parser = EntryPageParser.new(agent.get(ConfigurationService.comments_url))
29
+ comments = []
30
+ pages.times do
31
+ lines = parser.get_lines
32
+ lines.each do |line|
33
+ comments << parse_comment(line)
34
+ end
35
+ next_url = parser.get_next_url || break
36
+ parser = EntryPageParser.new(agent.get(next_url))
37
+ end
38
+ return comments
39
+ end
40
+
41
+ def parse_comment(element)
42
+ text = ""
43
+ element.search("span.comment").first.children.each do |ch|
44
+ text = ch.inner_html.gsub(/<.{1,2}>/,"")
45
+ end
46
+ header = element.search("span.comhead").first
47
+ voting = VotingInfoParser.new(element.search("td/center/a"), header).parse
48
+ user_info = UserInfoParser.new(header).parse
49
+ reply_link = element.search("td[@class='default']/p//u//a").first
50
+ reply_url = reply_link['href'] if reply_link
51
+ return Comment.new(text, voting, user_info, reply_url)
52
+ end
53
+
54
+ def write_comment(page_url, comment)
55
+ require_authentication
56
+ form = agent.get(page_url).forms.first
57
+ form.text = comment
58
+ form.submit
59
+ return true
60
+ end
61
+
62
+ end
@@ -0,0 +1,28 @@
1
+
2
+ class ConfigurationService
3
+
4
+ def self.base_url
5
+ return "http://news.ycombinator.com/"
6
+ end
7
+
8
+ def self.new_url
9
+ return File.join(self.base_url, "newest")
10
+ end
11
+
12
+ def self.ask_url
13
+ return File.join(self.base_url, "ask")
14
+ end
15
+
16
+ def self.jobs_url
17
+ return File.join(self.base_url, "jobs")
18
+ end
19
+
20
+ def self.comments_url
21
+ return File.join(self.base_url, "newcomments")
22
+ end
23
+
24
+ def self.submit_url
25
+ return File.join(self.base_url, "submit")
26
+ end
27
+
28
+ end
@@ -0,0 +1,52 @@
1
+
2
+ class EntryService
3
+ include MechanizeContext
4
+
5
+ def get_entries(pages = 1, url = ConfigurationService.base_url)
6
+ parser = EntryPageParser.new(agent.get(url))
7
+ entry_infos = []
8
+ pages.times do
9
+ lines = parser.get_lines
10
+ (lines.length / 2).times do
11
+ entry_infos << EntryParser.new(lines.shift, lines.shift).parse
12
+ end
13
+ next_url = parser.get_next_url || break
14
+ parser = EntryPageParser.new(agent.get(next_url))
15
+ end
16
+ return entry_infos
17
+ end
18
+
19
+ def get_new_entries(pages = 1)
20
+ return get_entries(pages, ConfigurationService.new_url)
21
+ end
22
+
23
+ def get_questions(pages = 1)
24
+ return get_entries(pages, ConfigurationService.ask_url)
25
+ end
26
+
27
+ def get_jobs(pages = 1)
28
+ return get_entries(pages, ConfigurationService.jobs_url)
29
+ end
30
+
31
+ def submit(*args)
32
+ require_authentication
33
+ form = agent.get(ConfigurationService.submit_url).forms.first
34
+ submit_link(form, args[0], args[1]) if args.length == 2
35
+ submit_question(form, args[0]) if args.length == 1
36
+ return true
37
+ end
38
+
39
+ private
40
+ def submit_link(form, title, url)
41
+ form.t = title
42
+ form.u = url
43
+ form.submit
44
+ end
45
+
46
+ def submit_question(form, text)
47
+ form.x = text
48
+ form.submit
49
+ end
50
+
51
+
52
+ end
@@ -0,0 +1,26 @@
1
+
2
+ class LoginService
3
+ include MechanizeContext
4
+
5
+ def login(username, password)
6
+ raise "You are logged in already - logout first." if authenticated?
7
+ page = agent.get(ConfigurationService.base_url)
8
+ login_url = page.search(".pagetop/a").last['href'].sub("/","")
9
+ login_page = agent.get(ConfigurationService.base_url + login_url)
10
+ form = login_page.forms.first
11
+ form.u = username
12
+ form.p = password
13
+ page = form.submit
14
+ return page.title != nil
15
+ end
16
+
17
+ def logout
18
+ require_authentication
19
+ page = agent.get(ConfigurationService.base_url)
20
+ login_url = page.search(".pagetop/a").last['href'].sub("/","")
21
+ logout_page = agent.get(ConfigurationService.base_url + login_url)
22
+ agent.cookie_jar.jar.clear
23
+ return logout_page.search(".pagetop/a").last.inner_html == "login"
24
+ end
25
+
26
+ end
@@ -0,0 +1,28 @@
1
+
2
+ module MechanizeContext
3
+
4
+ @@contexts = {}
5
+
6
+ def self.agent=(key)
7
+ @@default = key
8
+ end
9
+
10
+ def agent
11
+ @@default ||= :default
12
+ @@contexts[@@default] = Mechanize.new unless @@contexts[@@default]
13
+ return @@contexts[@@default]
14
+ end
15
+
16
+ def [](key)
17
+ return @@contexts[key]
18
+ end
19
+
20
+ def require_authentication
21
+ raise NotAuthenticatedError unless authenticated?
22
+ end
23
+
24
+ def authenticated?(key = :default)
25
+ return @@contexts[key].cookie_jar.jar.any?
26
+ end
27
+
28
+ end
@@ -0,0 +1,8 @@
1
+
2
+ class NotAuthenticatedError < StandardError
3
+
4
+ def message
5
+ return "You need to authenticate before making this operation"
6
+ end
7
+
8
+ end
@@ -0,0 +1,16 @@
1
+ class CommentsInfoParser
2
+
3
+ def initialize(comments_element)
4
+ @element = comments_element.search("a")[1]
5
+ end
6
+
7
+ def parse
8
+ comments_info = nil
9
+ if @element
10
+ comments = @element.inner_html.split[0].to_i
11
+ comments_page = @element['href']
12
+ comments_info = CommentsInfo.new(comments, comments_page)
13
+ end
14
+ return comments_info
15
+ end
16
+ end
@@ -0,0 +1,24 @@
1
+
2
+ class EntryPageParser
3
+
4
+ def initialize(page)
5
+ @page = page
6
+ end
7
+
8
+ def get_lines
9
+ lines = @page.search("//table")[2].search("tr").select do |tr|
10
+ tr['style'] !~ /height/ &&
11
+ tr.children.first.attributes.count != 0
12
+ end
13
+ more_link = lines.last.search("a").first
14
+ lines.pop if more_link && more_link.inner_html == "More"
15
+ return lines
16
+ end
17
+
18
+ def get_next_url
19
+ more_link = @page.search("//table")[2].search("tr/td/a").select { |node| node.inner_html == "More"}.first
20
+ return more_link['href'] if more_link
21
+ return nil
22
+ end
23
+
24
+ end
@@ -0,0 +1,19 @@
1
+
2
+ class EntryParser
3
+
4
+ def initialize(first_line, second_line)
5
+ @first_line = first_line
6
+ @second_line = second_line
7
+ end
8
+
9
+ def parse
10
+ number = @first_line.search("[@class='title']")[0].inner_html.sub(".","").to_i
11
+ link = LinkInfoParser.new(@first_line.search("[@class='title']")[1]).parse
12
+ voting = VotingInfoParser.new(@first_line.search("td/center/a"), @second_line.search("[@class='subtext']")[0]).parse
13
+ user = UserInfoParser.new(@second_line.search("[@class='subtext']")[0]).parse
14
+ comments = CommentsInfoParser.new(@second_line.search("[@class='subtext']")[0]).parse
15
+ time = TimeInfoParser.new(@second_line.search("[@class='subtext']").children[3]).parse
16
+ return Entry.new(number, link, voting, user, comments, time)
17
+ end
18
+
19
+ end
@@ -0,0 +1,17 @@
1
+
2
+ class LinkInfoParser
3
+
4
+ def initialize(link_element)
5
+ @element = link_element
6
+ end
7
+
8
+ def parse
9
+ link = @element.search("a")[0]['href']
10
+ title = @element.search("a")[0].inner_html
11
+ site_element = @element.search("span")
12
+ site = site_element.inner_html.sub("(","").sub(")","").strip if site_element.any?
13
+ return LinkInfo.new(title, link, site)
14
+ end
15
+
16
+
17
+ end
@@ -0,0 +1,15 @@
1
+
2
+ class TimeInfoParser
3
+
4
+ def initialize(time_element)
5
+ @element = time_element
6
+ end
7
+
8
+ def parse
9
+ value = @element.text.strip.split[0].to_i
10
+ unit_of_measure = @element.text.strip.split[1]
11
+ return TimeInfo.new(value, unit_of_measure)
12
+ end
13
+ end
14
+
15
+
@@ -0,0 +1,13 @@
1
+
2
+ class UserInfoParser
3
+
4
+ def initialize(user_element)
5
+ @element = user_element
6
+ end
7
+
8
+ def parse
9
+ user_name = @element.search("a")[0].inner_html
10
+ user_page = @element.search("a")[0]['href']
11
+ return UserInfo.new(user_name, user_page)
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+
2
+ class VotingInfoParser
3
+
4
+ def initialize(voting_element, score_element)
5
+ @voting_element = voting_element
6
+ @score_element = score_element
7
+ end
8
+
9
+ def parse
10
+ upvote = @voting_element[0]['href'] if @voting_element[0]
11
+ downvote = @voting_element[1]['href'] if @voting_element[1]
12
+ score = @score_element.search("span")[0].inner_html.split[0].to_i
13
+ return VotingInfo.new(score, upvote, downvote)
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+
2
+ class VotingService
3
+ include MechanizeContext
4
+
5
+ def vote(url)
6
+ require_authentication
7
+ agent.get(url)
8
+ return true
9
+ end
10
+
11
+ end
@@ -0,0 +1,8 @@
1
+
2
+ require 'rubygems'
3
+ require 'mechanize'
4
+
5
+ require 'require_all'
6
+
7
+ require_all File.join(File.dirname(__FILE__), 'HNAPI', 'domain')
8
+ require_all File.join(File.dirname(__FILE__), 'HNAPI', 'services')
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-hackernews
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease: false
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 0
10
+ version: 1.0.0
11
+ platform: ruby
12
+ authors:
13
+ - Andrea Dallera
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-09-08 00:00:00 +02:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: require_all
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 19
30
+ segments:
31
+ - 1
32
+ - 1
33
+ - 0
34
+ version: 1.1.0
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: mechanize
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 23
46
+ segments:
47
+ - 1
48
+ - 0
49
+ - 0
50
+ version: 1.0.0
51
+ type: :runtime
52
+ version_requirements: *id002
53
+ description: An interface to Hacker News
54
+ email: andrea@andreadallera.com
55
+ executables: []
56
+
57
+ extensions: []
58
+
59
+ extra_rdoc_files: []
60
+
61
+ files:
62
+ - README.rdoc
63
+ - Rakefile
64
+ - lib/HNAPI/domain/comment/comment.rb
65
+ - lib/HNAPI/domain/user.rb
66
+ - lib/HNAPI/domain/entry/link_info.rb
67
+ - lib/HNAPI/domain/entry/voting_info.rb
68
+ - lib/HNAPI/domain/entry/time_info.rb
69
+ - lib/HNAPI/domain/entry/user_info.rb
70
+ - lib/HNAPI/domain/entry/entry.rb
71
+ - lib/HNAPI/domain/entry/comments_info.rb
72
+ - lib/HNAPI/services/parsers/link_info_parser.rb
73
+ - lib/HNAPI/services/parsers/entry_parser.rb
74
+ - lib/HNAPI/services/parsers/voting_info_parser.rb
75
+ - lib/HNAPI/services/parsers/user_info_parser.rb
76
+ - lib/HNAPI/services/parsers/time_info_parser.rb
77
+ - lib/HNAPI/services/parsers/entry_page_parser.rb
78
+ - lib/HNAPI/services/parsers/comments_info_parser.rb
79
+ - lib/HNAPI/services/voting_service.rb
80
+ - lib/HNAPI/services/mechanize_context.rb
81
+ - lib/HNAPI/services/comment_service.rb
82
+ - lib/HNAPI/services/login_service.rb
83
+ - lib/HNAPI/services/entry_service.rb
84
+ - lib/HNAPI/services/not_authenticated_error.rb
85
+ - lib/HNAPI/services/configuration_service.rb
86
+ - lib/ruby-hackernews.rb
87
+ has_rdoc: true
88
+ homepage: http://github.com/bolthar/ruby-hackernews
89
+ licenses: []
90
+
91
+ post_install_message:
92
+ rdoc_options: []
93
+
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ hash: 3
102
+ segments:
103
+ - 0
104
+ version: "0"
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ hash: 3
111
+ segments:
112
+ - 0
113
+ version: "0"
114
+ requirements: []
115
+
116
+ rubyforge_project:
117
+ rubygems_version: 1.3.7
118
+ signing_key:
119
+ specification_version: 3
120
+ summary: An interface to Hacker News
121
+ test_files: []
122
+