ruby-hackernews 1.0.0

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