├── lib ├── ruby-hackernews │ ├── version.rb │ ├── services │ │ ├── text_service.rb │ │ ├── not_authenticated_error.rb │ │ ├── voting_service.rb │ │ ├── parsers │ │ │ ├── text_parser.rb │ │ │ ├── link_info_parser.rb │ │ │ ├── user_info_parser.rb │ │ │ ├── time_info_parser.rb │ │ │ ├── voting_info_parser.rb │ │ │ ├── comments_info_parser.rb │ │ │ ├── entry_page_parser.rb │ │ │ └── entry_parser.rb │ │ ├── page_fetcher.rb │ │ ├── signup_service.rb │ │ ├── mechanize_context.rb │ │ ├── user_info_service.rb │ │ ├── login_service.rb │ │ ├── configuration_service.rb │ │ ├── entry_service.rb │ │ └── comment_service.rb │ └── domain │ │ ├── entry │ │ ├── user_info.rb │ │ ├── link_info.rb │ │ ├── voting_info.rb │ │ ├── comments_info.rb │ │ ├── time_info.rb │ │ └── entry.rb │ │ ├── user.rb │ │ └── comment │ │ └── comment.rb └── ruby-hackernews.rb ├── Rakefile ├── spec ├── spec_helper.rb └── HNAPI │ ├── services │ ├── entries │ │ ├── parsers │ │ │ ├── user_info_parser_spec.rb │ │ │ ├── time_info_parser_spec.rb │ │ │ ├── entry_page_parser_spec.rb │ │ │ ├── comments_info_parser_spec.rb │ │ │ ├── link_info_parser_spec.rb │ │ │ ├── voting_info_parser_spec.rb │ │ │ ├── entry_parser_spec.rb │ │ │ ├── parser_helper.rb │ │ │ └── hn_test_page.html │ │ └── entry_service_spec.rb │ └── mechanize_context_spec.rb │ └── domain │ └── time_info_spec.rb ├── .gitignore ├── Gemfile ├── ruby-hackernews.gemspec └── README.rdoc /lib/ruby-hackernews/version.rb: -------------------------------------------------------------------------------- 1 | module RubyHackernews 2 | VERSION = "1.4.0" 3 | end 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new('spec') 7 | 8 | task :default => :spec -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.join(File.dirname(__FILE__),'..','lib') 2 | 3 | require 'rubygems' 4 | require 'bundler/setup' 5 | 6 | require 'ruby-hackernews.rb' 7 | 8 | -------------------------------------------------------------------------------- /lib/ruby-hackernews/services/text_service.rb: -------------------------------------------------------------------------------- 1 | module RubyHackernews 2 | 3 | class TextService 4 | include MechanizeContext 5 | 6 | def get_text(page) 7 | return TextParser.new(page.search("table")[2]).parse 8 | end 9 | 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | 19 | nbproject 20 | *~ 21 | *.save* -------------------------------------------------------------------------------- /lib/ruby-hackernews/services/not_authenticated_error.rb: -------------------------------------------------------------------------------- 1 | module RubyHackernews 2 | 3 | class NotAuthenticatedError < StandardError 4 | 5 | def message 6 | return "You need to authenticate before making this operation" 7 | end 8 | 9 | end 10 | 11 | end 12 | -------------------------------------------------------------------------------- /lib/ruby-hackernews/services/voting_service.rb: -------------------------------------------------------------------------------- 1 | module RubyHackernews 2 | 3 | class VotingService 4 | include MechanizeContext 5 | 6 | def vote(url) 7 | require_authentication 8 | agent.get(url) 9 | return true 10 | end 11 | 12 | end 13 | 14 | end 15 | -------------------------------------------------------------------------------- /lib/ruby-hackernews/domain/entry/user_info.rb: -------------------------------------------------------------------------------- 1 | module RubyHackernews 2 | 3 | class UserInfo 4 | 5 | attr_reader :name 6 | attr_reader :page 7 | 8 | def initialize(name, page) 9 | @name = name 10 | @page = page 11 | end 12 | 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /lib/ruby-hackernews.rb: -------------------------------------------------------------------------------- 1 | 2 | require "ruby-hackernews/version" 3 | 4 | require 'rubygems' 5 | require 'mechanize' 6 | 7 | require 'require_all' 8 | 9 | require_all File.join(File.dirname(__FILE__), 'ruby-hackernews', 'domain') 10 | require_all File.join(File.dirname(__FILE__), 'ruby-hackernews', 'services') 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | group :development do 4 | # rspec is here and not in gemspec, because :require is not supported there 5 | # put every other gem in gemspec 6 | gem 'rspec', :require => "spec" 7 | end 8 | 9 | # Specify your gem's dependencies in ruby-hackernews.gemspec 10 | gemspec 11 | -------------------------------------------------------------------------------- /lib/ruby-hackernews/services/parsers/text_parser.rb: -------------------------------------------------------------------------------- 1 | 2 | module RubyHackernews 3 | 4 | class TextParser 5 | 6 | def initialize(table) 7 | @target_line = table.search("tr")[3] 8 | end 9 | 10 | def parse 11 | return @target_line.search("td")[1].inner_text 12 | end 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/ruby-hackernews/domain/entry/link_info.rb: -------------------------------------------------------------------------------- 1 | module RubyHackernews 2 | 3 | class LinkInfo 4 | 5 | attr_reader :title 6 | attr_reader :href 7 | attr_reader :site 8 | 9 | def initialize(title, href, site) 10 | @title = title 11 | @href = href 12 | @site = site 13 | end 14 | 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /lib/ruby-hackernews/domain/entry/voting_info.rb: -------------------------------------------------------------------------------- 1 | module RubyHackernews 2 | 3 | class VotingInfo 4 | 5 | attr_reader :score 6 | attr_reader :upvote 7 | attr_reader :downvote 8 | 9 | def initialize(score, upvote, downvote) 10 | @score = score 11 | @upvote = upvote 12 | @downvote = downvote 13 | end 14 | 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /lib/ruby-hackernews/services/page_fetcher.rb: -------------------------------------------------------------------------------- 1 | 2 | module RubyHackernews 3 | 4 | #acts as a cache 5 | class PageFetcher 6 | include MechanizeContext 7 | 8 | def initialize(page_url) 9 | @url = page_url 10 | end 11 | 12 | def page 13 | unless @page 14 | @page = agent.get(@url) 15 | end 16 | return @page 17 | end 18 | 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/ruby-hackernews/domain/entry/comments_info.rb: -------------------------------------------------------------------------------- 1 | module RubyHackernews 2 | 3 | class CommentsInfo 4 | 5 | attr_accessor :count 6 | attr_reader :page 7 | 8 | def initialize(count, page) 9 | @count = count 10 | @page = page 11 | end 12 | 13 | def id 14 | return page[/\d+/] 15 | end 16 | 17 | def url 18 | return page 19 | end 20 | 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /lib/ruby-hackernews/services/parsers/link_info_parser.rb: -------------------------------------------------------------------------------- 1 | module RubyHackernews 2 | 3 | class LinkInfoParser 4 | 5 | def initialize(link_element) 6 | @element = link_element 7 | end 8 | 9 | def parse 10 | link = @element.search("a")[0]['href'] 11 | title = @element.search("a")[0].inner_html 12 | site_element = @element.search("span") 13 | site = site_element.inner_html.sub("(","").sub(")","").strip if site_element.any? 14 | return LinkInfo.new(title, link, site) 15 | end 16 | 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /lib/ruby-hackernews/services/parsers/user_info_parser.rb: -------------------------------------------------------------------------------- 1 | module RubyHackernews 2 | 3 | class UserInfoParser 4 | 5 | def initialize(second_line) 6 | @second_line = second_line 7 | end 8 | 9 | def parse 10 | return unless user_link 11 | 12 | user_name = user_link.inner_html 13 | user_page = user_link['href'] 14 | 15 | UserInfo.new(user_name, user_page) 16 | end 17 | 18 | private 19 | 20 | def user_link 21 | links.find { |link| link['href'] =~ /user\?id=/ } 22 | end 23 | 24 | def links 25 | @second_line.css('a') 26 | end 27 | 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /lib/ruby-hackernews/services/parsers/time_info_parser.rb: -------------------------------------------------------------------------------- 1 | module RubyHackernews 2 | 3 | class TimeInfoParser 4 | 5 | def initialize(second_line) 6 | @second_line = second_line 7 | end 8 | 9 | def parse 10 | return unless time_link 11 | 12 | value = time_link.text.strip.split[0].to_i 13 | unit_of_measure = time_link.text.strip.split[1] 14 | 15 | TimeInfo.new(value, unit_of_measure) 16 | end 17 | 18 | private 19 | 20 | def time_link 21 | links.find { |link| link.text =~ /\sago/ } 22 | end 23 | 24 | def links 25 | @second_line.css('a') 26 | end 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /lib/ruby-hackernews/services/signup_service.rb: -------------------------------------------------------------------------------- 1 | module RubyHackernews 2 | 3 | class SignupService 4 | include MechanizeContext 5 | 6 | def signup(username, password) 7 | raise "You are logged in already - logout first." if authenticated? 8 | page = agent.get(ConfigurationService.base_url) 9 | login_url = page.search(".pagetop/a").last['href'].sub("/","") 10 | login_page = agent.get(ConfigurationService.base_url + login_url) 11 | form = login_page.forms[1] 12 | form.acct = username 13 | form.pw = password 14 | page = form.submit 15 | return page.title != nil 16 | end 17 | 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /lib/ruby-hackernews/domain/entry/time_info.rb: -------------------------------------------------------------------------------- 1 | module RubyHackernews 2 | 3 | class TimeInfo 4 | 5 | SECOND = 1 6 | MINUTE = 60 * SECOND 7 | HOUR = 60 * MINUTE 8 | DAY = 24 * HOUR 9 | 10 | def time 11 | return Time.now - @unit_of_measure * @value 12 | end 13 | 14 | def initialize(value, unit_of_measure) 15 | @value = value 16 | if(unit_of_measure) 17 | descriptor = unit_of_measure 18 | descriptor = unit_of_measure[0..-2] if unit_of_measure[-1].chr == 's' 19 | @unit_of_measure = self.class.const_get(descriptor.upcase) 20 | end 21 | end 22 | 23 | 24 | end 25 | 26 | end 27 | -------------------------------------------------------------------------------- /lib/ruby-hackernews/services/parsers/voting_info_parser.rb: -------------------------------------------------------------------------------- 1 | module RubyHackernews 2 | 3 | class VotingInfoParser 4 | 5 | def initialize(voting_element, score_element) 6 | @voting_element = voting_element 7 | @score_element = score_element 8 | end 9 | 10 | def parse 11 | upvote = @voting_element[0]['href'] if @voting_element[0] 12 | downvote = @voting_element[1]['href'] if @voting_element[1] 13 | points_element = @score_element.search("span")[0] 14 | score = points_element.inner_html.split[0].to_i if points_element 15 | return VotingInfo.new(score, upvote, downvote) 16 | end 17 | 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /lib/ruby-hackernews/services/parsers/comments_info_parser.rb: -------------------------------------------------------------------------------- 1 | module RubyHackernews 2 | 3 | class CommentsInfoParser 4 | 5 | def initialize(second_line) 6 | @second_line = second_line 7 | end 8 | 9 | def parse 10 | return unless comments_link 11 | 12 | comments = comments_link.text.split[0].to_i 13 | comments_page = comments_link['href'] 14 | 15 | CommentsInfo.new(comments, comments_page) 16 | end 17 | 18 | private 19 | 20 | def comments_link 21 | links.find { |link| link.text =~ /comment|discuss/ } 22 | end 23 | 24 | def links 25 | @second_line.css('a') 26 | end 27 | 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /lib/ruby-hackernews/services/mechanize_context.rb: -------------------------------------------------------------------------------- 1 | module RubyHackernews 2 | 3 | module MechanizeContext 4 | 5 | @@contexts = {} 6 | 7 | def self.agent=(key) 8 | @@default = key 9 | end 10 | 11 | def agent 12 | @@default ||= :default 13 | @@contexts[@@default] = Mechanize.new unless @@contexts[@@default] 14 | return @@contexts[@@default] 15 | end 16 | 17 | def [](key) 18 | return @@contexts[key] 19 | end 20 | 21 | def require_authentication 22 | raise NotAuthenticatedError unless authenticated? 23 | end 24 | 25 | def authenticated?(key = :default) 26 | return @@contexts[key] && @@contexts[key].cookie_jar.jar.any? 27 | end 28 | 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /lib/ruby-hackernews/services/parsers/entry_page_parser.rb: -------------------------------------------------------------------------------- 1 | module RubyHackernews 2 | 3 | class EntryPageParser 4 | 5 | def initialize(page) 6 | @page = page 7 | end 8 | 9 | def get_lines 10 | lines = @page.search("//table")[2].search("tr").select do |tr| 11 | tr['style'] !~ /height/ 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 25 | 26 | end 27 | -------------------------------------------------------------------------------- /spec/HNAPI/services/entries/parsers/user_info_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require File.expand_path(File.dirname(__FILE__) + '/parser_helper') 3 | 4 | module RubyHackernews 5 | describe UserInfoParser do 6 | 7 | before :each do 8 | @parser = UserInfoParser.new(ParserHelper.second_line) 9 | end 10 | 11 | describe :parse do 12 | 13 | it "should pass the right user name" do 14 | UserInfo.should_receive(:new).with("johnthedebs", anything) 15 | @parser.parse 16 | end 17 | 18 | it "should pass the right user page" do 19 | UserInfo.should_receive(:new).with(anything, "user?id=johnthedebs") 20 | @parser.parse 21 | end 22 | 23 | end 24 | end 25 | end 26 | 27 | -------------------------------------------------------------------------------- /spec/HNAPI/services/entries/parsers/time_info_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require File.join(File.dirname(__FILE__), 'parser_helper') 3 | 4 | module RubyHackernews 5 | describe TimeInfoParser do 6 | 7 | before :each do 8 | @parser = TimeInfoParser.new(ParserHelper.second_line) 9 | end 10 | 11 | describe :parse do 12 | 13 | it "should pass the right value as the first parameter" do 14 | TimeInfo.should_receive(:new).with(4, anything) 15 | @parser.parse 16 | end 17 | 18 | it "should pass the right UoM descriptor as the second parameter" do 19 | TimeInfo.should_receive(:new).with(anything, "hours") 20 | @parser.parse 21 | end 22 | 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/HNAPI/services/entries/parsers/entry_page_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require File.expand_path(File.dirname(__FILE__) + '/parser_helper') 3 | 4 | module RubyHackernews 5 | describe EntryPageParser do 6 | 7 | before :each do 8 | @page = ParserHelper.full_page 9 | end 10 | 11 | describe :get_lines do 12 | it "should always return 60 elements" do 13 | lines = EntryPageParser.new(@page).get_lines 14 | lines.length.should == 60 15 | end 16 | end 17 | 18 | describe :get_next_url do 19 | it "should get the url in 'More' element" do 20 | url = EntryPageParser.new(@page).get_next_url 21 | url.should == "/x?fnid=9LO1ba1swy" 22 | end 23 | end 24 | 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/ruby-hackernews/domain/user.rb: -------------------------------------------------------------------------------- 1 | 2 | module RubyHackernews 3 | 4 | class User 5 | 6 | attr_reader :name 7 | 8 | def initialize(name) 9 | @name = name 10 | end 11 | 12 | def login(password) 13 | return LoginService.new.login(@name, password) 14 | end 15 | 16 | def signup(password) 17 | return SignupService.new.signup(@name, password) 18 | end 19 | 20 | def submissions(pages = 1) 21 | return UserInfoService.new.submissions(@name, pages) 22 | end 23 | 24 | def saved(pages = 1) 25 | return UserInfoService.new.saved(@name, pages) 26 | end 27 | 28 | def logout 29 | return LoginService.new.logout 30 | end 31 | 32 | def comments(pages = 1) 33 | return UserInfoService.new.comments(@name, pages) 34 | end 35 | 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /lib/ruby-hackernews/services/user_info_service.rb: -------------------------------------------------------------------------------- 1 | module RubyHackernews 2 | 3 | class UserInfoService 4 | include MechanizeContext 5 | 6 | def submissions(username, pages = 1) 7 | page_url = ConfigurationService.base_url + 'submitted?id=' + username 8 | return EntryService.new.get_entries(pages, page_url) 9 | end 10 | 11 | def saved(username, pages = 1) 12 | require_authentication 13 | page_url = ConfigurationService.base_url + 'saved?id=' + username 14 | return EntryService.new.get_entries(pages, page_url) 15 | end 16 | 17 | ##This doesn't work, need a new commentService to handle the user comment page 18 | def comments(username, pages = 1) 19 | page_url = ConfigurationService.base_url + 'threads?id=' + username 20 | return CommentService.new.get_new_comments(pages,page_url) 21 | end 22 | 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /lib/ruby-hackernews/services/login_service.rb: -------------------------------------------------------------------------------- 1 | module RubyHackernews 2 | 3 | class LoginService 4 | include MechanizeContext 5 | 6 | def login(username, password) 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.acct = username 12 | form.pw = 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 27 | 28 | end 29 | -------------------------------------------------------------------------------- /lib/ruby-hackernews/services/configuration_service.rb: -------------------------------------------------------------------------------- 1 | module RubyHackernews 2 | 3 | class ConfigurationService 4 | 5 | def self.base_url=(url) 6 | @base_url = url 7 | end 8 | 9 | def self.base_url 10 | return @base_url || "http://news.ycombinator.com/" 11 | end 12 | 13 | def self.new_url 14 | return File.join(self.base_url, "newest") 15 | end 16 | 17 | def self.ask_url 18 | return File.join(self.base_url, "ask") 19 | end 20 | 21 | def self.jobs_url 22 | return File.join(self.base_url, "jobs") 23 | end 24 | 25 | def self.show_url 26 | return File.join(self.base_url, "show") 27 | end 28 | 29 | def self.new_shows_url 30 | return File.join(self.base_url, "shownew") 31 | end 32 | 33 | def self.comments_url 34 | return File.join(self.base_url, "newcomments") 35 | end 36 | 37 | def self.submit_url 38 | return File.join(self.base_url, "submit") 39 | end 40 | 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /ruby-hackernews.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'ruby-hackernews/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "ruby-hackernews" 8 | spec.version = RubyHackernews::VERSION 9 | spec.authors = ["Andrea Dallera"] 10 | spec.email = ["andrea@andreadallera.com"] 11 | spec.description = %q{An interface to Hacker News} 12 | spec.summary = %q{An interface to Hacker News} 13 | spec.homepage = "http://github.com/bolthar/ruby-hackernews" 14 | 15 | spec.files = `git ls-files`.split($/) 16 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 17 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 18 | spec.require_paths = ["lib"] 19 | 20 | spec.has_rdoc = false 21 | 22 | spec.add_development_dependency "bundler", "~> 1.3" 23 | spec.add_development_dependency "rake" 24 | 25 | spec.add_dependency('require_all', '>= 1.1.0') 26 | spec.add_dependency('mechanize', '>= 1.0.0') 27 | end 28 | -------------------------------------------------------------------------------- /lib/ruby-hackernews/services/parsers/entry_parser.rb: -------------------------------------------------------------------------------- 1 | module RubyHackernews 2 | 3 | class EntryParser 4 | 5 | def initialize(first_line, second_line) 6 | @first_line = first_line 7 | @second_line = second_line 8 | end 9 | 10 | def parse 11 | first_line_title = @first_line.search("[@class='title']") 12 | number_segment = nil 13 | link_segment = nil 14 | if(first_line_title.length > 1) 15 | number_segment = first_line_title[0] 16 | link_segment = first_line_title[1] 17 | else 18 | link_segment = first_line_title[0] 19 | end 20 | number = number_segment.inner_html.sub(".","").to_i if number_segment 21 | link = LinkInfoParser.new(link_segment).parse 22 | voting = VotingInfoParser.new(@first_line.search("td/center/a"), @second_line.search("[@class='subtext']")[0]).parse 23 | user = UserInfoParser.new(@second_line).parse 24 | comments = CommentsInfoParser.new(@second_line).parse 25 | time = TimeInfoParser.new(@second_line).parse 26 | return Entry.new(number, link, voting, user, comments, time) 27 | end 28 | 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /spec/HNAPI/services/entries/parsers/comments_info_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require File.expand_path(File.dirname(__FILE__) + '/parser_helper') 3 | 4 | module RubyHackernews 5 | describe CommentsInfoParser do 6 | 7 | before(:each) do 8 | @line = ParserHelper.second_line 9 | @comment_link = "item?id=1645686" 10 | end 11 | 12 | describe :parse do 13 | 14 | it "should not call CommentsInfo.new if comment tag is missing" do 15 | parser = CommentsInfoParser.new(ParserHelper.second_line_comments_tag_missing) 16 | CommentsInfo.should_not_receive(:new) 17 | parser.parse 18 | end 19 | 20 | it "should call CommentsInfo.new with comments count if not 'discuss'" do 21 | parser = CommentsInfoParser.new(@line) 22 | CommentsInfo.should_receive(:new).with(2, anything) 23 | parser.parse 24 | end 25 | 26 | it "should call CommentsInfo.new with 0 if is 'discuss'" do 27 | parser = CommentsInfoParser.new(ParserHelper.second_line_no_comments_yet) 28 | CommentsInfo.should_receive(:new).with(0, anything) 29 | parser.parse 30 | end 31 | 32 | it "should call CommentsInfo.new with comment link" do 33 | parser = CommentsInfoParser.new(@line) 34 | CommentsInfo.should_receive(:new).with(anything, @comment_link) 35 | parser.parse 36 | end 37 | 38 | end 39 | 40 | end 41 | end 42 | 43 | -------------------------------------------------------------------------------- /spec/HNAPI/services/mechanize_context_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module RubyHackernews 4 | describe MechanizeContext do 5 | 6 | before :each do 7 | MechanizeContext.send(:class_variable_set, :@@contexts, nil) 8 | MechanizeContext.send(:class_variable_set, :@@default, nil) 9 | end 10 | 11 | describe "agent=" do 12 | 13 | it "should set @@default as passed key" do 14 | MechanizeContext.agent = :test_agent 15 | MechanizeContext.send(:class_variable_get, :@@default).should == :test_agent 16 | end 17 | 18 | end 19 | 20 | describe "agent" do 21 | 22 | it "should return the @@default agent" do 23 | MechanizeContext.send(:class_variable_set, :@@default, :test_value) 24 | MechanizeContext.send(:class_variable_set, :@@contexts, {:test_value => :target}) 25 | klass = Class.new 26 | klass.instance_eval do 27 | include MechanizeContext 28 | end 29 | klass.new.agent.should == :target 30 | end 31 | 32 | end 33 | 34 | describe "[]" do 35 | 36 | it "should return the [key] agent" do 37 | MechanizeContext.send(:class_variable_set, :@@contexts, {:test_value => :target}) 38 | klass = Class.new 39 | klass.instance_eval do 40 | include MechanizeContext 41 | end 42 | klass.new[:test_value].should == :target 43 | end 44 | 45 | end 46 | end 47 | end 48 | 49 | -------------------------------------------------------------------------------- /spec/HNAPI/domain/time_info_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module RubyHackernews 4 | describe TimeInfo do 5 | 6 | before :all do 7 | class RubyHackernews::TimeInfo 8 | DUMMY = 10 9 | end 10 | end 11 | 12 | describe :initialize do 13 | 14 | it "should set unit_of_measure as uppercase descriptor const if singular" do 15 | timeinfo = TimeInfo.new(5, "dummy") 16 | timeinfo.instance_variable_get(:@unit_of_measure).should == 10 17 | end 18 | 19 | it "should set unit_of_measure as uppercase minus last s descriptor const if plural" do 20 | timeinfo = TimeInfo.new(5, "dummys") 21 | timeinfo.instance_variable_get(:@unit_of_measure).should == 10 22 | end 23 | 24 | end 25 | 26 | describe :time do 27 | 28 | it "should return right amount when passed seconds" do 29 | TimeInfo.new(15, "seconds").time.round.should == (Time.now - 15).round 30 | end 31 | 32 | it "should return right amount when passed minutes" do 33 | TimeInfo.new(24, "minutes").time.round.should == (Time.now - 24*60).round 34 | end 35 | 36 | it "should return right amount when passed hours" do 37 | TimeInfo.new(3, "hours").time.round.should == (Time.now - 3*3600).round 38 | end 39 | 40 | it "should return right amount when passed days" do 41 | TimeInfo.new(16, "days").time.round.should == (Time.now - 16*86400).round 42 | end 43 | 44 | end 45 | end 46 | end 47 | 48 | -------------------------------------------------------------------------------- /spec/HNAPI/services/entries/parsers/link_info_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require File.expand_path(File.dirname(__FILE__) + '/parser_helper') 3 | 4 | module RubyHackernews 5 | describe LinkInfoParser do 6 | 7 | before :each do 8 | @line = ParserHelper.link_line 9 | @correct_title = "Flow chart: How to find out which things to throw out" 10 | @correct_link = "http://mortenjust.com/2010/08/30/finding-out-which-things-to-throw-out/" 11 | @correct_site = "mortenjust.com" 12 | end 13 | 14 | describe :parse do 15 | 16 | it "should call LinkInfo.new with correct title" do 17 | parser = LinkInfoParser.new(@line) 18 | LinkInfo.should_receive(:new).with(@correct_title, anything, anything) 19 | parser.parse 20 | end 21 | 22 | it "should call LinkInfo.new with correct link" do 23 | parser = LinkInfoParser.new(@line) 24 | LinkInfo.should_receive(:new).with(anything, @correct_link, anything) 25 | parser.parse 26 | end 27 | 28 | it "should call LinkInfo.new with correct site if site is present" do 29 | parser = LinkInfoParser.new(@line) 30 | LinkInfo.should_receive(:new).with(anything, anything, @correct_site) 31 | parser.parse 32 | end 33 | 34 | it "should call LinkInfo.new with nil if site is not present" do 35 | parser = LinkInfoParser.new(ParserHelper.link_line_no_site) 36 | LinkInfo.should_receive(:new).with(anything, anything, nil) 37 | parser.parse 38 | end 39 | 40 | end 41 | 42 | end 43 | end 44 | 45 | -------------------------------------------------------------------------------- /spec/HNAPI/services/entries/parsers/voting_info_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require File.expand_path(File.dirname(__FILE__) + '/parser_helper') 3 | 4 | module RubyHackernews 5 | describe VotingInfoParser do 6 | 7 | before :each do 8 | @voting_element = ParserHelper.voting_node 9 | @score_element = ParserHelper.second_line 10 | @correct_vote_link = "vote?for=1645686&dir=up&whence=%6e%65%77%73" 11 | end 12 | 13 | describe :parse do 14 | 15 | it "should assign link.href to upvote if element not nil" do 16 | parser = VotingInfoParser.new(@voting_element, @score_element) 17 | VotingInfo.should_receive(:new).with(anything, @correct_vote_link, anything) 18 | parser.parse 19 | end 20 | 21 | it "should assing nil to upvote if element nil" do 22 | parser = VotingInfoParser.new(ParserHelper.voting_node_no_upvote, @score_element) 23 | VotingInfo.should_receive(:new).with(anything, nil, anything) 24 | parser.parse 25 | end 26 | 27 | it "should assing link.href to downvote if element not nil" do 28 | parser = VotingInfoParser.new(@voting_element, @score_element) 29 | VotingInfo.should_receive(:new).with(anything, anything, @correct_vote_link) 30 | parser.parse 31 | end 32 | 33 | it "should assing nil to downvote if element nil" do 34 | parser = VotingInfoParser.new(ParserHelper.voting_node_no_downvote, @score_element) 35 | VotingInfo.should_receive(:new).with(anything, anything, nil) 36 | parser.parse 37 | end 38 | 39 | it "should assing correct score to score" do 40 | parser = VotingInfoParser.new(@voting_element, @score_element) 41 | VotingInfo.should_receive(:new).with(17, anything, anything) 42 | parser.parse 43 | end 44 | end 45 | 46 | end 47 | end 48 | 49 | -------------------------------------------------------------------------------- /spec/HNAPI/services/entries/entry_service_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | #useless purely integration tests... remove? 4 | module RubyHackernews 5 | describe EntryService do 6 | 7 | describe :get_entries do 8 | 9 | it "should always call EntryPageParser with current agent" do 10 | parser = Object.new 11 | allow(parser).to receive_messages(:get_lines => [], :get_next_url => nil) 12 | agent = Object.new 13 | allow(agent).to receive_messages(:get => :page) 14 | MechanizeContext.send(:class_variable_set, :@@contexts, {:default => agent}) 15 | service = EntryService.new 16 | expect(EntryPageParser).to receive(:new).at_least(1).with(:page).and_return(parser) 17 | service.get_entries 18 | end 19 | 20 | it "should always call get_lines 'pages' number of times" do 21 | pages = 5 22 | parser = Object.new 23 | expect(parser).to receive(:get_lines).and_return([]) 24 | allow(parser).to receive(:get_next_url).and_return(nil) 25 | agent = Object.new 26 | allow(agent).to receive_messages(:get => :page) 27 | MechanizeContext.send(:class_variable_set, :@@contexts, {:default => agent}) 28 | service = EntryService.new 29 | allow(EntryPageParser).to receive(:new).with(:page).and_return(parser) 30 | service.get_entries 31 | end 32 | 33 | it "should always call get_next_url 'pages' number of times" do 34 | parser = Object.new 35 | allow(parser).to receive(:get_lines).and_return([]) 36 | expect(parser).to receive(:get_next_url).and_return(nil) 37 | agent = Object.new 38 | allow(agent).to receive_messages(:get => :page) 39 | MechanizeContext.send(:class_variable_set, :@@contexts, {:default => agent}) 40 | service = EntryService.new 41 | allow(EntryPageParser).to receive(:new).with(:page).and_return(parser) 42 | service.get_entries 43 | end 44 | 45 | end 46 | end 47 | end 48 | 49 | -------------------------------------------------------------------------------- /spec/HNAPI/services/entries/parsers/entry_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require File.expand_path(File.dirname(__FILE__) + '/parser_helper') 3 | 4 | module RubyHackernews 5 | describe EntryParser do 6 | 7 | before(:each) do 8 | @first_line = ParserHelper.first_line_full 9 | @second_line = ParserHelper.second_line_full 10 | end 11 | 12 | describe :parse do 13 | 14 | it "should call Entry.new with the right number" do 15 | parser = EntryParser.new(@first_line, @second_line) 16 | Entry.should_receive(:new).with(2, anything, anything, anything, anything, anything) 17 | parser.parse 18 | end 19 | 20 | it "should return an entry with correct link" do 21 | parser = EntryParser.new(@first_line, @second_line) 22 | entry = parser.parse 23 | entry.link.title.should == "The Tragic Death of Practically Everything" 24 | entry.link.href.should == "http://technologizer.com/2010/08/18/the-tragic-death-of-practically-everything/" 25 | entry.link.site.should == "technologizer.com" 26 | end 27 | 28 | it "should return an entry with correct voting" do 29 | parser = EntryParser.new(@first_line, @second_line) 30 | entry = parser.parse 31 | entry.voting.upvote.should == "vote?for=1645745&dir=up&whence=%6e%65%77%73" 32 | entry.voting.downvote.should be_nil 33 | entry.voting.score.should == 59 34 | end 35 | 36 | it "should return an entry with correct user" do 37 | parser = EntryParser.new(@first_line, @second_line) 38 | entry = parser.parse 39 | entry.user.name.should == "dhotson" 40 | entry.user.page.should == "user?id=dhotson" 41 | end 42 | 43 | it "should return an entry with correct time" do 44 | parser = EntryParser.new(@first_line, @second_line) 45 | entry = parser.parse 46 | entry.time.round.should == (Time.now - 3600*4).round 47 | end 48 | 49 | end 50 | 51 | end 52 | end 53 | 54 | -------------------------------------------------------------------------------- /lib/ruby-hackernews/domain/comment/comment.rb: -------------------------------------------------------------------------------- 1 | module RubyHackernews 2 | 3 | class Comment 4 | include Enumerable 5 | 6 | attr_reader :text_html 7 | attr_reader :voting 8 | attr_reader :user 9 | 10 | attr_accessor :parent 11 | 12 | def initialize(text_html, voting, user_info, reply_link, absolute_link, parent_link) 13 | @text_html = text_html 14 | @voting = voting 15 | @user = user_info 16 | @reply_link = reply_link 17 | @absolute_link = absolute_link 18 | @parent_link = parent_link 19 | @children = [] 20 | end 21 | 22 | def id 23 | return @absolute_link.split("=")[1].to_i 24 | end 25 | 26 | def parent_id 27 | if parent 28 | parent.id 29 | elsif @parent_link 30 | @parent_link[/\d+/].to_i 31 | end 32 | end 33 | 34 | def text 35 | @text ||= text_html.gsub(/<.{1,2}>/, "") 36 | end 37 | 38 | def <<(comment) 39 | comment.parent = self 40 | @children << comment 41 | end 42 | 43 | def each(&block) 44 | @children.each(&block) 45 | end 46 | 47 | def <=>(other_comment) 48 | return other_comment.voting.score <=> @voting.score 49 | end 50 | 51 | def method_missing(method, *args, &block) 52 | @children.send(method, *args, &block) 53 | end 54 | 55 | def self.newest(pages = 1) 56 | return CommentService.new.get_new_comments(pages) 57 | end 58 | 59 | def self.newest_with_extra(pages = 1, url = nil) 60 | args = [pages] 61 | args << url unless url.nil? 62 | return CommentService.new.get_new_comments_with_extra *args 63 | end 64 | 65 | def self.find(id) 66 | return CommentService.new.find_by_id(id) 67 | end 68 | 69 | def reply(text) 70 | return false unless @reply_link 71 | CommentService.new.write_comment(@reply_link, text) 72 | return true 73 | end 74 | 75 | def upvote 76 | VotingService.new.vote(@voting.upvote) 77 | end 78 | 79 | def downvote 80 | VotingService.new.vote(@voting.downvote) 81 | end 82 | 83 | end 84 | 85 | end 86 | -------------------------------------------------------------------------------- /lib/ruby-hackernews/services/entry_service.rb: -------------------------------------------------------------------------------- 1 | module RubyHackernews 2 | 3 | class EntryService 4 | include MechanizeContext 5 | 6 | def get_entries(pages = 1, url = ConfigurationService.base_url) 7 | get_entries_with_extra(pages, url)[:entries] 8 | end 9 | 10 | def get_entries_with_extra(pages = 1, url = ConfigurationService.base_url) 11 | parser = EntryPageParser.new(agent.get(url)) 12 | entry_infos = [] 13 | next_url = nil 14 | pages.times do 15 | lines = parser.get_lines 16 | while lines.first["class"] != "athing" 17 | lines.shift 18 | end 19 | (lines.length / 2).times do 20 | entry_infos << EntryParser.new(lines.shift, lines.shift).parse 21 | end 22 | next_url = parser.get_next_url || break 23 | parser = EntryPageParser.new(agent.get(next_url)) 24 | end 25 | return {:entries => entry_infos, :next_url => next_url} 26 | end 27 | 28 | def find_by_id(id) 29 | page = agent.get(ConfigurationService.base_url + "item?id=#{id}") 30 | lines = page.search("table")[2].search("tr") 31 | return EntryParser.new(lines[0], lines[1]).parse 32 | end 33 | 34 | def get_new_entries(pages = 1) 35 | return get_entries(pages, ConfigurationService.new_url) 36 | end 37 | 38 | def get_questions(pages = 1) 39 | return get_entries(pages, ConfigurationService.ask_url) 40 | end 41 | 42 | def get_jobs(pages = 1) 43 | return get_entries(pages, ConfigurationService.jobs_url) 44 | end 45 | 46 | def get_shows(pages = 1) 47 | return get_entries(pages, ConfigurationService.show_url) 48 | end 49 | 50 | def get_new_shows(pages = 1) 51 | return get_entries(pages, ConfigurationService.new_shows_url) 52 | end 53 | 54 | def submit(title, url) 55 | require_authentication 56 | form = agent.get(ConfigurationService.submit_url).forms.first 57 | submit_link(form, title, url) 58 | return true 59 | end 60 | 61 | def ask(title, text) 62 | require_authentication 63 | form = agent.get(ConfigurationService.submit_url).forms.first 64 | submit_question(form, title, text) 65 | return true 66 | end 67 | 68 | private 69 | def submit_link(form, title, url) 70 | form.t = title 71 | form.u = url 72 | form.submit 73 | end 74 | 75 | def submit_question(form, title, text) 76 | form.t = title 77 | form.x = text 78 | form.submit 79 | end 80 | 81 | end 82 | 83 | end 84 | -------------------------------------------------------------------------------- /lib/ruby-hackernews/domain/entry/entry.rb: -------------------------------------------------------------------------------- 1 | module RubyHackernews 2 | 3 | class Entry 4 | 5 | attr_reader :number 6 | attr_reader :link 7 | attr_reader :voting 8 | attr_reader :user 9 | 10 | def initialize(number, link, voting, user, comments, time) 11 | @number = number 12 | @link = link 13 | @voting = voting 14 | @user = user 15 | @time = time 16 | @comments_info = comments 17 | if @comments_info 18 | @cache = PageFetcher.new(@comments_info.page) 19 | else 20 | @cache = PageFetcher.new(@link.href) 21 | end 22 | end 23 | 24 | def text 25 | unless @text 26 | @text = TextService.new.get_text(@cache.page) 27 | end 28 | return @text 29 | end 30 | 31 | def comments 32 | unless @comments 33 | @comments = CommentService.new.get_comments(@cache.page) 34 | end 35 | return @comments 36 | end 37 | 38 | def self.all(pages = 1) 39 | return EntryService.new.get_entries(pages) 40 | end 41 | 42 | def self.newest(pages = 1) 43 | return EntryService.new.get_new_entries(pages) 44 | end 45 | 46 | def self.newest_with_extra(pages = 1, url = nil) 47 | args = [pages] 48 | args << url unless url.nil? 49 | return EntryService.new.get_entries_with_extra *args 50 | end 51 | 52 | def self.questions(pages = 1) 53 | return EntryService.new.get_questions(pages) 54 | end 55 | 56 | def self.jobs(pages = 1) 57 | return EntryService.new.get_jobs(pages) 58 | end 59 | 60 | def self.shows(pages = 1) 61 | return EntryService.new.get_shows(pages) 62 | end 63 | 64 | def self.new_shows(pages = 1) 65 | return EntryService.new.get_new_shows(pages) 66 | end 67 | 68 | def self.find(id) 69 | return EntryService.new.find_by_id(id) 70 | end 71 | 72 | def time 73 | return @time.time 74 | end 75 | 76 | def id 77 | return @comments_info ? @comments_info.id : nil 78 | end 79 | 80 | def comments_url 81 | return @comments_info ? ConfigurationService.base_url + @comments_info.url : nil 82 | end 83 | 84 | def comments_count 85 | return @comments_info.count unless @comments_info.nil? 86 | end 87 | 88 | def write_comment(text) 89 | return CommentService.new.write_comment(@comments_info.page, text) 90 | end 91 | 92 | def self.submit(title, url) 93 | return EntryService.new.submit(title, url) 94 | end 95 | 96 | def self.ask(title, text) 97 | return EntryService.new.ask(title, text) 98 | end 99 | 100 | def upvote 101 | return VotingService.new.vote(@voting.upvote) 102 | end 103 | 104 | end 105 | 106 | end 107 | -------------------------------------------------------------------------------- /lib/ruby-hackernews/services/comment_service.rb: -------------------------------------------------------------------------------- 1 | module RubyHackernews 2 | 3 | class CommentService 4 | include MechanizeContext 5 | 6 | def get_comments(page) 7 | table = page.search("table")[3] 8 | return get_comments_entities(table) 9 | end 10 | 11 | def get_user_comments(user) 12 | page = agent.get(ConfigurationService.base_url + "threads?id=#{user.name}") 13 | table = page.search("table")[2] 14 | return get_comments_entities(table) 15 | end 16 | 17 | def find_by_id(id) 18 | page = agent.get(ConfigurationService.base_url + "item?id=#{id}") 19 | comment = parse_comment(page.search("table")[2].search("tr").first) 20 | comment.instance_variable_set(:@absolute_link, "item?id=#{id}") 21 | get_comments_entities(page.search("table")[3]).each do |c| 22 | comment << c 23 | end 24 | return comment 25 | end 26 | 27 | def get_comments_entities(table) 28 | comments = [] 29 | target = comments 30 | current_level = 0 31 | trs = table.search("table/tr").select do |tr| 32 | tr.search("span.comment").inner_html != "[deleted]" 33 | end 34 | trs.each do |tr| 35 | comment = parse_comment(tr) 36 | level = tr.search("img[@src='s.gif']").first['width'].to_i / 40 37 | difference = current_level - level 38 | (difference + 1).times do 39 | target = target.kind_of?(Comment) && target.parent ? target.parent : comments 40 | end 41 | current_level = level 42 | target << comment 43 | target = comment 44 | end 45 | return comments 46 | end 47 | 48 | def get_new_comments(pages = 1, url = ConfigurationService.comments_url) 49 | get_new_comments_with_extra(pages,url)[:comments] 50 | end 51 | 52 | def get_new_comments_with_extra(pages = 1, url = ConfigurationService.comments_url) 53 | parser = EntryPageParser.new(agent.get(url)) 54 | comments = [] 55 | next_url = nil 56 | pages.times do 57 | lines = parser.get_lines 58 | lines.each do |line| 59 | comments << parse_comment(line) 60 | end 61 | next_url = parser.get_next_url || break 62 | parser = EntryPageParser.new(agent.get(next_url)) 63 | end 64 | return {:comments => comments, :next_url => next_url} 65 | end 66 | 67 | def parse_comment(element) 68 | text_html = element.search("span.comment").first.search("font").children.map { |x| x.inner_text }.join("\n") 69 | header = element.search("span.comhead").first 70 | voting = VotingInfoParser.new(element.search("td/center/a"), header).parse 71 | user_info = UserInfoParser.new(header).parse 72 | reply_link = element.search("td[@class='default']/p//u//a").first 73 | reply_url = reply_link['href'] if reply_link 74 | absolute_link_group = header.search("a") 75 | absolute_url = absolute_link_group.count == 2 ? absolute_link_group[1]['href'] : nil 76 | parent_link = header.search("a[text()*='parent']").first 77 | parent_url = parent_link['href'] if parent_link 78 | return Comment.new(text_html, voting, user_info, reply_url, absolute_url, parent_url) 79 | end 80 | 81 | def write_comment(page_url, comment) 82 | require_authentication 83 | form = agent.get(page_url).forms.first 84 | form.text = comment 85 | form.submit 86 | return true 87 | end 88 | 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /spec/HNAPI/services/entries/parsers/parser_helper.rb: -------------------------------------------------------------------------------- 1 | 2 | class ParserHelper 3 | 4 | #upvote: vote?for=1645686&dir=up&whence=%6e%65%77%73 5 | #downvote: nil 6 | def self.voting_node_no_downvote 7 | return Nokogiri::HTML.fragment('' 8 | ).children 9 | end 10 | 11 | #upvote: nil 12 | #downvote: vote?for=1645686&dir=up&whence=%6e%65%77%73 13 | def self.voting_node_no_upvote 14 | return Nokogiri::HTML.fragment('' 15 | ).children 16 | end 17 | 18 | #upvote: vote?for=1645686&dir=up&whence=%6e%65%77%73 19 | #downvote: vote?for=1645686&dir=up&whence=%6e%65%77%73 20 | def self.voting_node 21 | return Nokogiri::HTML.fragment('' 22 | ).children 23 | end 24 | 25 | #username : johnthedebs 26 | #user page : user?id=johnthedebs 27 | #score : 17 28 | #comments : 2 29 | #comment_page : item?id=1645686 30 | def self.second_line 31 | return Nokogiri::HTML.fragment('17 points by johnthedebs 4 hours ago | 2 comments') 32 | end 33 | 34 | #username : johnthedebs 35 | #user page : user?id=johnthedebs 36 | #score : 17 37 | #comment_page : item?id=1645686 38 | def self.second_line_no_comments_yet 39 | return Nokogiri::HTML.fragment('17 points by johnthedebs 4 hours ago | discuss') 40 | end 41 | 42 | #username : johnthedebs 43 | #user page : user?id=johnthedebs 44 | #score : 17 45 | def self.second_line_comments_tag_missing 46 | return Nokogiri::HTML.fragment('17 points by johnthedebs 4 hours ago') 47 | end 48 | 49 | #title : Flow chart: How to find out which things to throw out 50 | #link : http://mortenjust.com/2010/08/30/finding-out-which-things-to-throw-out/ 51 | #site : (mortenjust.com) 52 | def self.link_line 53 | return Nokogiri::HTML('Flow chart: How to find out which things to throw out (mortenjust.com) ') 54 | end 55 | 56 | #title : Flow chart: How to find out which things to throw out 57 | #link : http://mortenjust.com/2010/08/30/finding-out-which-things-to-throw-out/ 58 | def self.link_line_no_site 59 | return Nokogiri::HTML('Flow chart: How to find out which things to throw out') 60 | end 61 | 62 | def self.first_line_full 63 | return Nokogiri::HTML('2.
The Tragic Death of Practically Everything (technologizer.com) ') 64 | end 65 | 66 | def self.second_line_full 67 | Nokogiri::HTML.fragment('59 points by dhotson 4 hours ago | 5 comments') 68 | end 69 | 70 | def self.full_page 71 | File.open(File.join(File.dirname(__FILE__), 'hn_test_page.html'), 'r') do |file| 72 | return Nokogiri::HTML(file) 73 | end 74 | 75 | end 76 | 77 | end 78 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | An API over Hacker News 2 | 3 | http://stillmaintained.com/bolthar/ruby-hackernews.png 4 | 5 | == Requirements 6 | 7 | mechanize (>= 1.0.0) 8 | require_all (>= 1.1.0) 9 | 10 | == Installation 11 | 12 | gem install ruby-hackernews 13 | 14 | then, in your script: 15 | 16 | require 'ruby-hackernews' 17 | 18 | before using it. If you want to include the namespace (RubyHackernews), add 19 | 20 | include RubyHackernews 21 | 22 | otherwise, you'll have to access the gem's classes adding the module, like this: 23 | 24 | RubyHackernews::Entry.all 25 | 26 | == Entries 27 | 28 | You can get entries on the main page with: 29 | 30 | Entry.all # returns the main page's entries as an array 31 | 32 | You can provide a number of pages: 33 | 34 | Entry.all(3) # will return the entries on the first 3 pages 35 | 36 | There are methods for getting specific entry types: 37 | 38 | Entry.questions # gets the first page of questions (ask HN) 39 | Entry.newest # gets the first page of new links (new) 40 | Entry.jobs # gets the first page of job offerts (jobs) 41 | Entry.shows # gets the first page of shows (show HN) 42 | Entry.new_shows # gets the first page of new shows (show HN new) 43 | 44 | You can also get a single entry by its ID: 45 | 46 | Entry.find(3102321) # gets that specific entry (warning: number will be 0!) 47 | 48 | Each Entry instance has the following data: 49 | 50 | entry = Entry.all.first # gets the top entry on the mainpage 51 | 52 | entry.number # the entry's position on HN 53 | 54 | entry.link.title # the link's name on HN 55 | entry.link.href # the actual link 56 | entry.link.site # the referring site, if any 57 | 58 | entry.voting.score # the entry's score on HN 59 | 60 | entry.user.name # the submitter's user name 61 | 62 | entry.time # the elapsed time from submission 63 | 64 | entry.text # the text of the submission (ask/jobs only) 65 | # NOTE: it will fetch the inner page 66 | 67 | After you've logged in (see below) you can do the following 68 | 69 | entry.upvote # votes the entry 70 | entry.write_comment("mycomment") # adds a comment to the entry 71 | Entry.submit("mytitle", "myurl") # submit a new link 72 | Entry.submit("myquestion", "question text") # submit a new question 73 | 74 | user.submissions.first.comments_url # returns the url to the current user's comments 75 | 76 | == Comments 77 | 78 | You get an entry's comments with: 79 | 80 | entry.comments 81 | 82 | You can also get a specific comment by id with: 83 | 84 | Comment.find("1234") # returns the comments with id 1234, 85 | # and its subcomments. 86 | 87 | Note that the method above will send a request to HN. If you just need the comments' count or url, you can instead use: 88 | 89 | entry.comments_count 90 | 91 | and 92 | 93 | entry.comments_url 94 | 95 | Either of which will not issue a request to HN's site. 96 | 97 | You can also get the newest comments on HN with: 98 | 99 | Comments.newest 100 | Comments.newest(3) # gets the first 3 pages of new comments 101 | 102 | Each Comment instance has the following data: 103 | 104 | comment = Entry.all.first.comments.first # gets the first comment of the first entry on HN's main page 105 | 106 | comment.id # comment's unique identifier 107 | comment.text # comment's body 108 | comment.user.name # poster's user name on HN 109 | comment.voting.score # comment's score 110 | 111 | Comments are enumerable and threaded, so you can do like: 112 | 113 | comment[2][0] # gets the third reply to this comment, then the first reply to the reply 114 | comment.first # gets the first reply to this comment 115 | comment.select do |c| # gets all the comment replies which text contains "test" 116 | text ~= /test/ 117 | end 118 | comment.parent # gets the comment's parent (nil if no parent) 119 | 120 | Once you're logged in (see below), you can do the following: 121 | 122 | comment.upvote 123 | comment.downvote 124 | comment.reply("mycomment") 125 | 126 | == Logging in 127 | 128 | You define a user with: 129 | 130 | user = User.new("username") 131 | 132 | Then, you log in with: 133 | 134 | user.login("password") 135 | 136 | Or, you can create a new use with that name: 137 | 138 | user.signup("password") # don't abuse this! 139 | 140 | You will be also logged in with the new user. So, no need to call user#login after user#signup. 141 | 142 | You can log out with: 143 | 144 | user.logout 145 | 146 | You have to log out before logging in with a different user. 147 | 148 | You can also get the current users submission list by doing this: 149 | 150 | user.submissions 151 | 152 | This will return a new Entry instance. For example: 153 | 154 | user.submissions.first.comment_url 155 | 156 | Will return the HN comment url of the last submitted story of that user 157 | 158 | == TO DO 159 | 160 | Get user info (comments, saved) 161 | Change user info/settings 162 | 163 | == THANKS TO 164 | 165 | - Anton Katunin ( https://github.com/antulik ) for adding the gemfile and putting all specs into modules 166 | - Mike Kelly ( https://github.com/mikekelly ) for fixing a bug in TimeInfo.time 167 | - Marc Köhlbrugge ( http://github.com/marckohlbrugge ) fox fixing and formatting the documentation 168 | - Peter Cooper ( http://github.com/peterc ) for bug reports 169 | - Peter Boctor ( http://github.com/boctor ) for fixing a bug about authentication 170 | - Josh Ellington ( http://github.com/joshellington ) for reporting a bug about job entries 171 | - Wayne ( http://github.com/BlissOfBeing ) for adding User.submissions, Entry.comments_url and cleaning up the Rakefile 172 | - Daniel Da Cunha ( http://github.com/ddacunha ) for a fix on Entry#comments_count 173 | - Nathan Campos ( http://github.com/nathanpc) for adding #text to Entry 174 | - Girish Sonawane ( https://github.com/girishso) for adding Entry#shows and Entry#new_shows methods 175 | - Niels Van Aken ( https://github.com/nvaken ) for a bugfix in EntryPageParser 176 | - Pedro Lambert ( https://github.com/p-lambert ) for fixing bugs and improving structure of CommentInfoParser, TimeInfoParser and UserInfoParser 177 | - FreedomBen ( https://github.com/FreedomBen ) for fixing a typo in the readme 178 | - Davide Santangelo ( https://github.com/davidesantangelo ) for reporting a bug related to jobs and show HN entries 179 | -------------------------------------------------------------------------------- /spec/HNAPI/services/entries/parsers/hn_test_page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hacker News
Hacker Newsnew | comments | ask | jobs | submitlogin
1.
Seaswarm: we can clean up the Gulf in a month (hackaday.com)
30 points by IgorPartola 1 hour ago | 10 comments
2.
The Tragic Death of Practically Everything (technologizer.com)
59 points by dhotson 4 hours ago | 5 comments
3.
I wrote an LLVM-powered trace-based JIT for Brainf**k (github.com)
31 points by Halienja 3 hours ago | 21 comments
4.
Jason Fried: The Truth About Real Estate (inc.com)
6 points by slater 27 minutes ago | discuss
5.
Rails 3.0: It's ready (rubyonrails.org)
313 points by steveklabnik 14 hours ago | 51 comments
6.
Python Visual Tutorial (mit.edu)
5 points by prog 23 minutes ago | discuss
7.
Flow chart: How to find out which things to throw out (mortenjust.com)
14 points by mortenjust 2 hours ago | 6 comments
8.
A Hit Song on YouTube, Unnameable on the Radio (nytimes.com)
17 points by donohoe 2 hours ago | 10 comments
9.
Swarmation: like musical chairs for pixels (swarmation.com)
162 points by mcantelon 13 hours ago | 59 comments
10.
Django Dash 2010 Results (djangodash.com)
21 points by johnthedebs 5 hours ago | 5 comments
11.
AMD to Retire the ATI Brand Later this Year (anandtech.com)
50 points by spcmnspff 8 hours ago | 9 comments
12.
Hacker builds working 1/10th scale Cray 1 (nycresistor.com)
61 points by henning 10 hours ago | 13 comments
13.
Spreading Hayek, Spurning Keynes (wsj.com)
7 points by DanielBMarkham 2 hours ago | 8 comments
14.
The GPL and Principles (ianbicking.org)
10 points by dhotson 3 hours ago | discuss
15.
Ageism in silicon valley: a foreign perspective (karaten.posterous.com)
73 points by karaten 11 hours ago | 20 comments
16.
The Most Important Business Lessons I’ve Learned (ryanallis.com)
42 points by zaidf 9 hours ago | 8 comments
17.
RubyDoc.info: Auto-generated RubyGems and GitHub library docs (rubydoc.info)
41 points by zapnap 10 hours ago | 3 comments
18.
What happens when you give homeless people a prepaid credit card. (thestar.com)
330 points by pavs 1 day ago | 219 comments
19.
Microsoft Academic Search (microsoft.com)
40 points by brisance 11 hours ago | 12 comments
20.
MapRejuice - Distributed Client Side Computing (Node Knockout Entry) (nodeknockout.com)
19 points by Klonoar 7 hours ago | 14 comments
21.
Regrets of the Dying (inspirationandchai.com)
304 points by vijaydev 1 day ago | 78 comments
22.
Net Neutrality is now law in Chile (mailfighter.net)
42 points by nfriedly 12 hours ago | 6 comments
23.
Realtime monitoring of Wikipedia edits via node.js (no.de)
83 points by mcantelon 17 hours ago | 11 comments
24.
Obesity: Drink till you drop (economist.com)
110 points by jsyedidia 20 hours ago | 81 comments
25.
Less Framework 2.0 Released (lessframework.com)
141 points by alexkiwi 22 hours ago | 26 comments
26.
Basic Command-Line Data Processing in Linux (symkat.com)
55 points by symkat 14 hours ago | 17 comments
27.
Cisco May Be Making A Run For Skype (techcrunch.com)
27 points by mjfern 11 hours ago | 13 comments
28.
The Right Kind of Ambition (bhorowitz.com)
39 points by dwynings 12 hours ago | 20 comments
29.
Arrington Is Denied Injunction Against Fusion Garage Over CrunchPad (scribd.com)
40 points by tptacek 14 hours ago | 12 comments
30.
Satoshi Kon's last words (makikoitoh.com)
79 points by wallflower 22 hours ago | 10 comments
More

27 |
Lists | RSS | Search | Bookmarklet | Guidelines | FAQ | News News | Feature Requests | Y Combinator | Apply | Library

28 | Analytics by Mixpanel
36 |
--------------------------------------------------------------------------------