├── .ruby-gemset ├── .ruby-version ├── Rakefile ├── .rspec ├── lib ├── virility │ ├── version.rb │ ├── exceptions.rb │ ├── strategies │ │ ├── stumble_upon.rb │ │ ├── pinterest.rb │ │ ├── linkedin.rb │ │ ├── plus_one.rb │ │ ├── reddit.rb │ │ └── facebook.rb │ ├── supporter.rb │ ├── strategy.rb │ └── excitation.rb └── virility.rb ├── Gemfile ├── .gitignore ├── LICENSE.txt ├── spec ├── spec_helper.rb ├── strategies │ ├── plus_one_spec.rb │ ├── pinterest_spec.rb │ ├── linkedin_spec.rb │ ├── reddit_spec.rb │ ├── stumble_upon_spec.rb │ └── facebook_spec.rb ├── strategy_spec.rb ├── excitation_spec.rb └── virility_spec.rb ├── virility.gemspec └── README.md /.ruby-gemset: -------------------------------------------------------------------------------- 1 | virility 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-2.3 2 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | --profile 4 | -------------------------------------------------------------------------------- /lib/virility/version.rb: -------------------------------------------------------------------------------- 1 | module Virility 2 | VERSION = "0.4.0" 3 | end 4 | -------------------------------------------------------------------------------- /lib/virility/exceptions.rb: -------------------------------------------------------------------------------- 1 | module Virility 2 | 3 | class UnknownStrategy < StandardError; end 4 | 5 | end -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rake' 4 | 5 | # Specify your gem's dependencies in virility.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /.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 | .byebug_history 19 | -------------------------------------------------------------------------------- /lib/virility/strategies/stumble_upon.rb: -------------------------------------------------------------------------------- 1 | module Virility 2 | class StumbleUpon < Strategy 3 | 4 | parser( 5 | Proc.new do |body, format| 6 | MultiJson.decode(body)["result"] 7 | end 8 | ) 9 | 10 | def census 11 | self.class.get("http://www.stumbleupon.com/services/1.01/badge.getinfo?url=#{@url}") 12 | end 13 | 14 | def count 15 | results["views"] || 0 16 | end 17 | 18 | end 19 | end -------------------------------------------------------------------------------- /lib/virility/strategies/pinterest.rb: -------------------------------------------------------------------------------- 1 | module Virility 2 | class Pinterest < Strategy 3 | 4 | parser( 5 | Proc.new do |body, format| 6 | MultiJson.decode(body.scan(/(\{.+\})/).flatten.first) 7 | end 8 | ) 9 | 10 | def census 11 | self.class.get("http://api.pinterest.com/v1/urls/count.json?url=#{@url}") 12 | end 13 | 14 | def count 15 | results["count"] || 0 16 | end 17 | 18 | end 19 | end -------------------------------------------------------------------------------- /lib/virility/strategies/linkedin.rb: -------------------------------------------------------------------------------- 1 | module Virility 2 | class Linkedin < Strategy 3 | 4 | parser( 5 | Proc.new do |body, format| 6 | MultiJson.decode(body.scan(/(\{.+\})/).flatten.first) 7 | end 8 | ) 9 | 10 | def census 11 | self.class.get("http://www.linkedin.com/countserv/count/share?url=#{@original_url}&format=json") 12 | end 13 | 14 | def count 15 | results[:count] || 0 16 | end 17 | 18 | end 19 | end -------------------------------------------------------------------------------- /lib/virility/strategies/plus_one.rb: -------------------------------------------------------------------------------- 1 | # http://stackoverflow.com/questions/7403553/how-do-i-get-the-counter-of-a-google-plus-1-button 2 | module Virility 3 | class PlusOne < Strategy 4 | 5 | parser( 6 | Proc.new do |body, format| 7 | {'shares' => body.scan(/c: (\d+)/).flatten.first} 8 | end 9 | ) 10 | 11 | def census 12 | self.class.get("https://plusone.google.com/_/+1/fastbutton?url=#{@url}") 13 | end 14 | 15 | def count 16 | results["shares"].to_i || 0 17 | end 18 | 19 | end 20 | end -------------------------------------------------------------------------------- /lib/virility/strategies/reddit.rb: -------------------------------------------------------------------------------- 1 | module Virility 2 | class Reddit < Strategy 3 | def outcome 4 | score = @response.parsed_response['data']['children'].map { |c| c['data']['score']}.reduce(:+) || 0 5 | { 'score' => score } 6 | end 7 | 8 | def census 9 | self.class.get("http://www.reddit.com/api/info.json?&url=#{@url}", http_proxyaddr: @http_proxyaddr, http_proxyport: @http_proxyport) 10 | end 11 | 12 | def count 13 | results['score'] || 0 14 | end 15 | 16 | private 17 | 18 | def valid_response_test 19 | @response.respond_to?(:parsed_response) && @response.parsed_response.is_a?(Hash) && !@response.parsed_response['data'].nil? && !@response.parsed_response['data']['children'].map { |c| c['data']['score']}.nil? 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/virility/strategies/facebook.rb: -------------------------------------------------------------------------------- 1 | module Virility 2 | class Facebook < Strategy 3 | BASE_URL = 'https://graph.facebook.com/?fields=share,og_object{engagement,title}&id='.freeze 4 | 5 | def census 6 | self.class.get("#{BASE_URL}#{@url}", http_proxyaddr: @http_proxyaddr, http_proxyport: @http_proxyport) 7 | end 8 | 9 | def outcome 10 | response = @response.parsed_response.dig('share') 11 | engagement = @response.parsed_response.dig('og_object', 'engagement') 12 | response['engagement_count'] = engagement.dig('count') 13 | response['social_sentence'] = engagement.dig('social_sentence') 14 | response 15 | end 16 | 17 | def count 18 | results.dig('engagement_count') || 0 19 | end 20 | 21 | private 22 | 23 | def valid_response_test 24 | @response.respond_to?(:parsed_response) && @response.parsed_response.is_a?(Hash) && !@response.parsed_response['share'].nil? 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/virility.rb: -------------------------------------------------------------------------------- 1 | require 'cgi' 2 | require 'httparty' 3 | require 'multi_json' 4 | 5 | require 'virility/version' 6 | require 'virility/supporter' 7 | require 'virility/excitation' 8 | require 'virility/strategy' 9 | require 'virility/exceptions' 10 | 11 | Dir["#{File.dirname(__FILE__)}/virility/strategies/**/*.rb"].each { |f| require f } 12 | 13 | module Virility 14 | 15 | # 16 | # Public API 17 | # 18 | 19 | def self.counts(url, strategies: [], proxy: {}) 20 | Virility::Excitation.new(url, strategies, proxy: proxy).counts 21 | end 22 | 23 | def self.total(url, strategies: [], proxy: {}) 24 | Virility::Excitation.new(url, strategies, proxy: proxy).total 25 | end 26 | 27 | def self.poll(url, strategies: [], proxy: {}) 28 | Virility::Excitation.new(url, strategies, proxy: proxy).poll 29 | end 30 | 31 | def self.url(url, strategies: [], proxy: {}) 32 | virility = Virility::Excitation.new(url, strategies, proxy: proxy) 33 | virility.poll 34 | virility 35 | end 36 | 37 | # 38 | # Factory 39 | # 40 | 41 | def self.factory(strategy, url, proxy = {}) 42 | Virility::Excitation.new(url).send(strategy, proxy) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/virility/supporter.rb: -------------------------------------------------------------------------------- 1 | module Virility 2 | module Supporter 3 | 4 | # 5 | # URL Encoding / Decoding Methods 6 | # 7 | 8 | def encode url 9 | CGI.escape url 10 | end 11 | 12 | def url 13 | CGI.unescape @url 14 | end 15 | 16 | def escaped_url 17 | @url 18 | end 19 | 20 | # 21 | # Camelize / Underscore 22 | # 23 | 24 | def camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true) 25 | if first_letter_in_uppercase 26 | lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase } 27 | else 28 | lower_case_and_underscored_word.first + camelize(lower_case_and_underscored_word)[1..-1] 29 | end 30 | end 31 | 32 | def underscore(camel_cased_word) 33 | word = camel_cased_word.to_s.dup 34 | word.gsub!(/::/, '/') 35 | word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2') 36 | word.gsub!(/([a-z\d])([A-Z])/,'\1_\2') 37 | word.tr!("-", "_") 38 | word.downcase! 39 | word 40 | end 41 | 42 | # 43 | # Convert Class Name To Appropriate Key Symbol 44 | # 45 | 46 | def symbolize_for_key(klass) 47 | underscore(klass.class.to_s.gsub(/Virility::/, '')).to_sym 48 | end 49 | 50 | def get_class_string(klass) 51 | File.basename(klass).gsub(/\.rb/,'') 52 | end 53 | 54 | end 55 | end -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Jay Sanders 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | THE BEER-WARE LICENSE (Revision 42): 26 | 27 | wrote this gem. As long as you retain this notice you 28 | can do whatever you want with this stuff. If we meet some day, and you think 29 | this stuff is worth it, you can buy me a beer in return. ~ Jay Sanders 30 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 3 | require 'rspec' 4 | require 'pry' 5 | require 'virility' 6 | 7 | # Requires supporting files with custom matchers and macros, etc, 8 | # in ./support/ and its subdirectories. 9 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} 10 | 11 | RSpec.configure do |config| 12 | config.filter_run :focus => true 13 | config.run_all_when_everything_filtered = true 14 | config.mock_with :rspec 15 | end 16 | 17 | # 18 | # Constants for Testing 19 | # 20 | 21 | module Virility 22 | TESTING_STRATEGIES = { 23 | :facebook => Virility::Facebook, 24 | :pinterest => Virility::Pinterest, 25 | :plus_one => Virility::PlusOne, 26 | :stumble_upon => Virility::StumbleUpon, 27 | :linkedin => Virility::Linkedin, 28 | :reddit => Virility::Reddit 29 | } 30 | FAKE_TESTING_STRATEGIES = [:digg, :instagram, :tumblr] 31 | FB_RESULTS = { 'comment_count' => '4', 'share_count' => '97173', 'engagement_count' => '97384', 'social_sentence' => "97K people like this."} 32 | FAKE_FB_RESULTS = [:face_count, :pages, :friends] 33 | end 34 | 35 | # 36 | # Example Groups 37 | # 38 | 39 | RSpec.shared_examples "no context results" do 40 | it "should not raise an error" do 41 | expect{ @virility.poll }.not_to raise_error 42 | end 43 | 44 | it "should return 0 for count" do 45 | expect(@virility.count).to eq(0) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /virility.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'virility/version' 5 | 6 | Gem::Specification.new do |gem| 7 | # Details 8 | gem.name = "virility" 9 | gem.version = Virility::VERSION 10 | gem.authors = ["Jay Sanders"] 11 | gem.email = ["jay@allieslabs.com"] 12 | gem.description = "Virility leverages the API's of many popular social services to collect data about the virility of a particular URL." 13 | gem.summary = "Virility calls upon the API's of many popular social services such as Facebook, Reddit and Pinterest to collect the number of likes, tweets and pins of a particular URL. Written with a modular construction, Virility makes it easy to drop new data collection strategies into the framework so that you can collect all of your statistics in one easy location." 14 | gem.homepage = "http://github.com/mindtonic/virility" 15 | gem.licenses = ['MIT', 'Beerware'] 16 | # Files 17 | gem.files = `git ls-files`.split($/) 18 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 19 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 20 | gem.require_paths = ["lib"] 21 | # Development 22 | gem.add_development_dependency "rspec", "~> 3.4" 23 | gem.add_development_dependency "pry", "~> 0.10" 24 | 25 | # Dependencies 26 | gem.add_dependency "httparty", "~> 0.11" 27 | gem.add_dependency "multi_json", "~> 1.11" 28 | end 29 | -------------------------------------------------------------------------------- /spec/strategies/plus_one_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | describe "Virility::PlusOne" do 4 | before(:each) do 5 | @url = "http://creativeallies.com" 6 | end 7 | 8 | describe "poll" do 9 | context "when there is not a valid result" do 10 | before(:each) do 11 | response = double("HTTParty::Response", :parsed_response => {"fake_return_value"=> "OICU812"}) 12 | allow(Virility::PlusOne).to receive(:get) { response } 13 | @virility = Virility::PlusOne.new(@url) 14 | end 15 | 16 | it_should_behave_like "no context results" 17 | end 18 | 19 | context "when there is no result" do 20 | before(:each) do 21 | response = double("HTTParty::Response") 22 | allow(Virility::PlusOne).to receive(:get) { response } 23 | @virility = Virility::PlusOne.new(@url) 24 | end 25 | 26 | it_should_behave_like "no context results" 27 | end 28 | 29 | context "when there is a result but no specific hash value" do 30 | before(:each) do 31 | response = double("HTTParty::Response", :parsed_response => {}) 32 | allow(Virility::PlusOne).to receive(:get) { response } 33 | @virility = Virility::PlusOne.new(@url) 34 | end 35 | 36 | it_should_behave_like "no context results" 37 | end 38 | 39 | context "when there is a result but parsed_response is weird" do 40 | before(:each) do 41 | response = double("HTTParty::Response", :parsed_response => Object.new) 42 | allow(Virility::PlusOne).to receive(:get) { response } 43 | @virility = Virility::PlusOne.new(@url) 44 | end 45 | 46 | it_should_behave_like "no context results" 47 | end 48 | 49 | context "when there is a valid result" do 50 | before(:each) do 51 | response = double("HTTParty::Response", :parsed_response => {"shares"=>"8"}) 52 | allow(Virility::PlusOne).to receive(:get) { response } 53 | @virility = Virility::PlusOne.new(@url) 54 | end 55 | 56 | it "should not raise an error" do 57 | expect{ @virility.poll }.not_to raise_error 58 | end 59 | 60 | it "should return 8 for the count" do 61 | expect(@virility.count).to eq(8) 62 | end 63 | end 64 | end 65 | 66 | end -------------------------------------------------------------------------------- /spec/strategies/pinterest_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | describe "Virility::Pinterest" do 4 | before(:each) do 5 | @url = "http://creativeallies.com" 6 | end 7 | 8 | describe "poll" do 9 | context "when there is not a valid result" do 10 | before(:each) do 11 | response = double("HTTParty::Response", :parsed_response => {"fake_return_value"=> "OICU812"}) 12 | allow(Virility::Pinterest).to receive(:get) { response } 13 | @virility = Virility::Pinterest.new(@url) 14 | end 15 | 16 | it_should_behave_like "no context results" 17 | end 18 | 19 | context "when there is no result" do 20 | before(:each) do 21 | response = double("HTTParty::Response") 22 | allow(Virility::Pinterest).to receive(:get) { response } 23 | @virility = Virility::Pinterest.new(@url) 24 | end 25 | 26 | it_should_behave_like "no context results" 27 | end 28 | 29 | context "when there is a result but no specific hash value" do 30 | before(:each) do 31 | response = double("HTTParty::Response", :parsed_response => {}) 32 | allow(Virility::Pinterest).to receive(:get) { response } 33 | @virility = Virility::Pinterest.new(@url) 34 | end 35 | 36 | it_should_behave_like "no context results" 37 | end 38 | 39 | context "when there is a result but parsed_response is weird" do 40 | before(:each) do 41 | response = double("HTTParty::Response", :parsed_response => Object.new) 42 | allow(Virility::Pinterest).to receive(:get) { response } 43 | @virility = Virility::Pinterest.new(@url) 44 | end 45 | 46 | it_should_behave_like "no context results" 47 | end 48 | 49 | context "when there is a valid result" do 50 | before(:each) do 51 | response = double("HTTParty::Response", :parsed_response => {"count"=>1, "url"=>"http://creativeallies.com"}) 52 | allow(Virility::Pinterest).to receive(:get) { response } 53 | @virility = Virility::Pinterest.new(@url) 54 | end 55 | 56 | it "should not raise an error" do 57 | expect{ @virility.poll }.not_to raise_error 58 | end 59 | 60 | it "should return 1 for the count" do 61 | expect(@virility.count).to eq(1) 62 | end 63 | end 64 | end 65 | 66 | end -------------------------------------------------------------------------------- /spec/strategies/linkedin_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | describe "Virility::Linkedin" do 4 | before(:each) do 5 | @url = "http://creativeallies.com" 6 | end 7 | 8 | describe "poll" do 9 | context "when there is not a valid result" do 10 | before(:each) do 11 | response = double("HTTParty::Response", :parsed_response => {"fake_return_value"=> "OICU812"}) 12 | allow(Virility::Linkedin).to receive(:get) { response } 13 | @virility = Virility::Linkedin.new(@url) 14 | end 15 | 16 | it_should_behave_like "no context results" 17 | end 18 | 19 | context "when there is no result" do 20 | before(:each) do 21 | response = double("HTTParty::Response") 22 | allow(Virility::Linkedin).to receive(:get) { response } 23 | @virility = Virility::Linkedin.new(@url) 24 | end 25 | 26 | it_should_behave_like "no context results" 27 | end 28 | 29 | context "when there is a result but no specific hash value" do 30 | before(:each) do 31 | response = double("HTTParty::Response", :parsed_response => {}) 32 | allow(Virility::Linkedin).to receive(:get) { response } 33 | @virility = Virility::Linkedin.new(@url) 34 | end 35 | 36 | it_should_behave_like "no context results" 37 | end 38 | 39 | context "when there is a result but parsed_response is weird" do 40 | before(:each) do 41 | response = double("HTTParty::Response", :parsed_response => Object.new) 42 | allow(Virility::Linkedin).to receive(:get) { response } 43 | @virility = Virility::Linkedin.new(@url) 44 | end 45 | 46 | it_should_behave_like "no context results" 47 | end 48 | 49 | context "when there is a valid result" do 50 | before(:each) do 51 | response = double("HTTParty::Response", :parsed_response => { "count":17, "fCnt":"17", "fCntPlusOne":"18", "url":"http:\/\/creativeallies.com" }) 52 | allow(Virility::Linkedin).to receive(:get) { response } 53 | @virility = Virility::Linkedin.new(@url) 54 | end 55 | 56 | it "should not raise an error" do 57 | expect{ @virility.poll }.not_to raise_error 58 | end 59 | 60 | it "should return 17 for the count" do 61 | expect(@virility.count).to eq(17) 62 | end 63 | end 64 | end 65 | end -------------------------------------------------------------------------------- /lib/virility/strategy.rb: -------------------------------------------------------------------------------- 1 | module Virility 2 | class Strategy 3 | include HTTParty 4 | include Virility::Supporter 5 | 6 | attr_accessor :url, :response, :results, :original_url, :http_proxyaddr, :http_proxyport 7 | 8 | def initialize(url, proxy: {}) 9 | @original_url = url 10 | @url = encode(url) 11 | @results = {} 12 | @http_proxyaddr = proxy.dig(:http_proxyaddr) 13 | @http_proxyport = proxy.dig(:http_proxyport) 14 | end 15 | 16 | # 17 | # Abstract Methods - Delete eventually 18 | # 19 | 20 | def census 21 | raise "Abstract Method census called on #{self.class} - Please define this method" 22 | end 23 | 24 | def count 25 | raise "Abstract Method count called on #{self.class} - Please define this method" 26 | end 27 | 28 | # 29 | # Poll 30 | # 31 | 32 | def poll 33 | call_strategy 34 | collect_results 35 | end 36 | 37 | # 38 | # Call Strategy 39 | # 40 | 41 | def call_strategy 42 | @response = census 43 | end 44 | 45 | # 46 | # Results 47 | # 48 | 49 | def collect_results 50 | if respond_to?(:outcome) 51 | @results = valid_response_test ? outcome : {} 52 | else 53 | @results = valid_response_test ? @response.parsed_response : {} 54 | end 55 | end 56 | 57 | def results 58 | if @results.empty? 59 | begin 60 | poll 61 | rescue => e 62 | puts "[virility#poll] #{self.class.to_s} => #{e}" 63 | end 64 | end 65 | @results 66 | end 67 | 68 | # 69 | # Dynamic Methods 70 | # 71 | 72 | def get_result key 73 | if result_exists?(key) 74 | results[key.to_s] 75 | else 76 | 0 77 | end 78 | end 79 | 80 | def result_exists? key 81 | !results[key.to_s].nil? 82 | end 83 | 84 | def method_missing(name, *args, &block) 85 | if result_exists?(name) 86 | get_result(name) 87 | else 88 | 0 89 | end 90 | end 91 | 92 | # 93 | # Parsed Response Test - Overwrite if needed 94 | # 95 | 96 | def valid_response_test 97 | @response.respond_to?(:parsed_response) and @response.parsed_response.is_a?(Hash) 98 | end 99 | 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /spec/strategies/reddit_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | describe "Virility::Reddit" do 4 | before(:each) do 5 | @url = "http://creativeallies.com" 6 | end 7 | 8 | describe "poll" do 9 | context "when there is not a valid result" do 10 | before(:each) do 11 | response = double("HTTParty::Response", :parsed_response => {"fake_return_value" => "OICU812"}) 12 | allow(Virility::Reddit).to receive(:get) { response } 13 | @virility = Virility::Reddit.new(@url) 14 | end 15 | it_should_behave_like "no context results" 16 | end 17 | 18 | context "when there is no result" do 19 | before(:each) do 20 | response = double("HTTParty::Response") 21 | allow(Virility::Reddit).to receive(:get) { response } 22 | @virility = Virility::Reddit.new(@url) 23 | end 24 | it_should_behave_like "no context results" 25 | end 26 | 27 | context "when there is a result but no specific hash value" do 28 | before(:each) do 29 | response = double("HTTParty::Response", :parsed_response => {}) 30 | allow(Virility::Reddit).to receive(:get) { response } 31 | @virility = Virility::Reddit.new(@url) 32 | end 33 | it_should_behave_like "no context results" 34 | end 35 | 36 | context "when there is a result but parsed_response is weird" do 37 | before(:each) do 38 | response = double("HTTParty::Response", :parsed_response => Object.new) 39 | allow(Virility::Reddit).to receive(:get) { response } 40 | @virility = Virility::Reddit.new(@url) 41 | end 42 | 43 | it_should_behave_like "no context results" 44 | end 45 | 46 | context "when there is a valid result" do 47 | before(:each) do 48 | response = double("HTTParty::Response", :parsed_response => 49 | { "kind" => "Listing", 50 | "data" => { 51 | "modhash" => "", 52 | "children" => 53 | [{ "kind" => "t3", "data" => { "score" => 35 } }] 54 | } 55 | } 56 | ) 57 | allow(Virility::Reddit).to receive(:get) { response } 58 | @virility = Virility::Reddit.new(@url) 59 | end 60 | 61 | it "should not raise an error" do 62 | expect{ @virility.poll }.not_to raise_error 63 | end 64 | 65 | it "should return 35 for the count" do 66 | expect(@virility.count).to eq(35) 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/strategies/stumble_upon_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | describe "Virility::StumbleUpon" do 4 | before(:each) do 5 | @url = "http://creativeallies.com" 6 | end 7 | 8 | describe "poll" do 9 | context "when there is not a valid result" do 10 | before(:each) do 11 | response = double("HTTParty::Response", :parsed_response => {"fake_return_value"=> "OICU812"}) 12 | allow(Virility::StumbleUpon).to receive(:get) { response } 13 | @virility = Virility::StumbleUpon.new(@url) 14 | end 15 | 16 | it_should_behave_like "no context results" 17 | end 18 | 19 | context "when there is no result" do 20 | before(:each) do 21 | response = double("HTTParty::Response") 22 | allow(Virility::StumbleUpon).to receive(:get) { response } 23 | @virility = Virility::StumbleUpon.new(@url) 24 | end 25 | 26 | it_should_behave_like "no context results" 27 | end 28 | 29 | context "when there is a result but no specific hash value" do 30 | before(:each) do 31 | response = double("HTTParty::Response", :parsed_response => {}) 32 | allow(Virility::StumbleUpon).to receive(:get) { response } 33 | @virility = Virility::StumbleUpon.new(@url) 34 | end 35 | 36 | it_should_behave_like "no context results" 37 | end 38 | 39 | context "when there is a result but parsed_response is weird" do 40 | before(:each) do 41 | response = double("HTTParty::Response", :parsed_response => Object.new) 42 | allow(Virility::StumbleUpon).to receive(:get) { response } 43 | @virility = Virility::StumbleUpon.new(@url) 44 | end 45 | 46 | it_should_behave_like "no context results" 47 | end 48 | 49 | context "when there is a valid result" do 50 | before(:each) do 51 | response = double("HTTParty::Response", :parsed_response => {"url"=>"http://creativeallies.com/", "in_index"=>true, "publicid"=>"2UhTwK", "views"=>4731, "title"=>"Creative Allies | Create Art For Rockstars | Upload For A Chance To Win", "thumbnail"=>"http://cdn.stumble-upon.com/mthumb/388/49348388.jpg", "thumbnail_b"=>"http://cdn.stumble-upon.com/images/nobthumb.png", "submit_link"=>"http://www.stumbleupon.com/submit/?url=http://creativeallies.com/", "badge_link"=>"http://www.stumbleupon.com/badge/?url=http://creativeallies.com/", "info_link"=>"http://www.stumbleupon.com/url/creativeallies.com/"}) 52 | allow(Virility::StumbleUpon).to receive(:get) { response } 53 | @virility = Virility::StumbleUpon.new(@url) 54 | end 55 | 56 | it "should not raise an error" do 57 | expect{ @virility.poll }.not_to raise_error 58 | end 59 | 60 | it "should return 4731 for the count" do 61 | expect(@virility.count).to eq(4731) 62 | end 63 | end 64 | end 65 | 66 | end -------------------------------------------------------------------------------- /lib/virility/excitation.rb: -------------------------------------------------------------------------------- 1 | module Virility 2 | class Excitation 3 | include Virility::Supporter 4 | 5 | attr_accessor :url, :results, :strategies, :counts, :proxy 6 | 7 | # 8 | # Initialization 9 | # 10 | def initialize(url, strategies = [], proxy: {}) 11 | @url = url 12 | @strategies = {} 13 | @results = {} 14 | @counts = {} 15 | @filter_strategies = strategies || [] 16 | @proxy = proxy 17 | collect_strategies 18 | filter_strategies 19 | end 20 | 21 | # 22 | # Get Virility from all of the Strategies 23 | # 24 | 25 | def poll 26 | if @results.empty? 27 | @strategies.each do |name, strategy| 28 | begin 29 | @results[symbolize_for_key(strategy)] = strategy.poll 30 | rescue => e 31 | puts "[virility#poll] #{strategy.class.to_s} => #{e}" 32 | end 33 | end 34 | end 35 | @results 36 | end 37 | 38 | def get_response(strategy) 39 | @strategies[strategy].response if @strategies[strategy] 40 | end 41 | 42 | # 43 | # Return Collected Counts as a Hash 44 | # 45 | 46 | def counts 47 | poll 48 | if @counts.empty? 49 | @strategies.each do |name, strategy| 50 | begin 51 | @counts[symbolize_for_key(strategy)] = strategy.count.to_i 52 | rescue => e 53 | puts "[virility] #{strategy.class.to_s} => #{e}" 54 | end 55 | end 56 | end 57 | @counts 58 | end 59 | 60 | def total_virility 61 | counts.values.inject(0) { |result, count| result + count } 62 | end 63 | alias :total :total_virility 64 | 65 | # 66 | # Gather all of the Strategies 67 | # 68 | 69 | def collect_strategies 70 | Dir["#{File.dirname(__FILE__)}/strategies/**/*.rb"].each { |klass| @strategies[get_class_string(klass).to_sym] = Virility.const_get(camelize(get_class_string(klass))).new(@url, proxy: @proxy) } 71 | end 72 | 73 | def filter_strategies 74 | return if @filter_strategies.empty? 75 | @strategies.select! { |k, _v| @filter_strategies.include?(k) } 76 | end 77 | 78 | def filter_strategies 79 | return if @filter_strategies.empty? 80 | @strategies.select! { |k, _v| @filter_strategies.include?(k) } 81 | end 82 | 83 | # 84 | # Reflection 85 | # 86 | 87 | def attributes 88 | {:url => @url, :available_strategies => @strategies.keys} 89 | end 90 | 91 | # 92 | # Dynamic Methods 93 | # 94 | 95 | def get_strategy strategy 96 | if strategy_exists?(strategy) 97 | @strategies[strategy.to_sym] 98 | else 99 | raise UnknownStrategy, "#{strategy} Is Not A Known Strategy" 100 | end 101 | end 102 | 103 | def strategy_exists? strategy 104 | !@strategies[strategy.to_sym].nil? 105 | end 106 | 107 | def method_missing(name, *args, &block) 108 | if strategy_exists?(name) 109 | get_strategy(name) 110 | else 111 | raise UnknownStrategy, "#{name} Is Not A Known Strategy" 112 | end 113 | end 114 | 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /spec/strategy_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/spec_helper') 2 | 3 | describe "Strategy" do 4 | before(:each) do 5 | @url = "http://creativeallies.com" 6 | end 7 | 8 | # 9 | # Initialization 10 | # 11 | 12 | context "initialization" do 13 | it "should raise an error if a URL is not set" do 14 | expect{Virility::Strategy.new}.to raise_error(ArgumentError, "wrong number of arguments (given 0, expected 1)") 15 | end 16 | 17 | it "should set and encode the url" do 18 | expect(Virility::Facebook.new(@url).url).to eq("http%3A%2F%2Fcreativeallies.com") 19 | end 20 | end 21 | 22 | # 23 | # Interface 24 | # 25 | 26 | context "interface" do 27 | it "should raise an error on poll" do 28 | expect{ Virility::Strategy.new(@url).poll }.to raise_error(RuntimeError, "Abstract Method census called on Virility::Strategy - Please define this method") 29 | end 30 | 31 | it "should raise an error on count" do 32 | expect{ Virility::Strategy.new(@url).count }.to raise_error(RuntimeError, "Abstract Method count called on Virility::Strategy - Please define this method") 33 | end 34 | end 35 | 36 | # 37 | # Dynamic Methods 38 | # 39 | 40 | describe "dynamic methods" do 41 | before(:each) do 42 | @virility = Virility::Facebook.new(@url) 43 | allow(@virility).to receive(:results) { Virility::FB_RESULTS } 44 | end 45 | 46 | context "overall testing" do 47 | Virility::FB_RESULTS.each do |key, value| 48 | it "should return #{value} when get_result is called with #{key}" do 49 | expect(@virility.send(key)).to eq(value) 50 | end 51 | end 52 | 53 | Virility::FAKE_FB_RESULTS.each do |key| 54 | it "should_not raise an error if the result (#{key}) does not exist" do 55 | expect{ @virility.send(key) }.not_to raise_error 56 | end 57 | 58 | it "should return 0 if the result (#{key}) does not exist" do 59 | expect(@virility.send(key)).to eq(0) 60 | end 61 | end 62 | end 63 | 64 | context "result_exists?" do 65 | before(:each) do 66 | @virility = Virility::Facebook.new(@url) 67 | allow(@virility).to receive(:results) { Virility::FB_RESULTS } 68 | end 69 | 70 | Virility::FB_RESULTS.keys.each do |result| 71 | it "should return true for #{result}" do 72 | expect(@virility.result_exists?(result)).to eq(true) 73 | end 74 | end 75 | 76 | Virility::FAKE_FB_RESULTS.each do |result| 77 | it "should return false for #{result}" do 78 | expect(@virility.result_exists?(result)).to eq(false) 79 | end 80 | end 81 | end 82 | 83 | context "get_result" do 84 | Virility::FB_RESULTS.each do |key, value| 85 | it "should return #{value} when get_result is called with #{key}" do 86 | expect(@virility.get_result(key)).to eq(value) 87 | end 88 | end 89 | 90 | Virility::FAKE_FB_RESULTS.each do |key| 91 | it "should_not raise an error if the result (#{key}) does not exist" do 92 | expect{ @virility.send(key) }.not_to raise_error 93 | end 94 | 95 | it "should return 0 if the result (#{key}) does not exist" do 96 | expect(@virility.send(key)).to eq(0) 97 | end 98 | end 99 | end 100 | 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /spec/excitation_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/spec_helper') 2 | 3 | describe "Excitation" do 4 | before(:each) do 5 | @url = "http://creativeallies.com" 6 | end 7 | 8 | # 9 | # Initialization 10 | # 11 | 12 | context "initialization" do 13 | it "should raise an error if a URL is not set" do 14 | expect{Virility::Excitation.new}.to raise_error(ArgumentError, "wrong number of arguments (given 0, expected 1..2)") 15 | end 16 | end 17 | 18 | # 19 | # Get Virility 20 | # 21 | 22 | context "poll" do 23 | it "should not raise an error" do 24 | expect{Virility::Excitation.new(@url).poll}.not_to raise_error 25 | end 26 | end 27 | 28 | # 29 | # Collect Strategies 30 | # 31 | 32 | context "collect_strategies" do 33 | it "should assign a hash to the strategies variable" do 34 | expect(Virility::Excitation.new(@url).strategies).to be_a_kind_of Hash 35 | end 36 | 37 | it "strategies should be inherited from the Strategy" do 38 | expect(Virility::Excitation.new(@url).strategies.first.last).to be_a_kind_of Virility::Strategy 39 | end 40 | 41 | it "should load all of the strategies" do 42 | expect(Virility::Excitation.new(@url).strategies.count).to eq(Dir[File.join('lib', 'virility', 'strategies', '**', '*')].count { |file| File.file?(file) }) 43 | end 44 | end 45 | 46 | # 47 | # Encode 48 | # 49 | 50 | context "encode" do 51 | it "should encode the url" do 52 | v = Virility::Excitation.new(@url) 53 | expect(v.encode(@url)).to eq("http%3A%2F%2Fcreativeallies.com") 54 | end 55 | end 56 | 57 | # 58 | # Symbolize For Key 59 | # 60 | 61 | context "symbolize_for_key" do 62 | it "should return a symbol with the name of the class" do 63 | expect(Virility::Excitation.new(@url).symbolize_for_key(Virility::Excitation.new(@url))).to eq(:excitation) 64 | end 65 | end 66 | 67 | # 68 | # Dynamic Methods 69 | # 70 | 71 | describe "dynamic methods" do 72 | context "overall testing" do 73 | Virility::TESTING_STRATEGIES.each do |method, klass| 74 | it "should return a #{klass} object when the method #{method} is called" do 75 | expect(Virility::Excitation.new(@url).send(method)).to be_a_kind_of(klass) 76 | end 77 | end 78 | 79 | Virility::FAKE_TESTING_STRATEGIES.each do |method| 80 | it "should raise an error if the strategy (#{method}) does not exist" do 81 | expect{ Virility::Excitation.new(@url).send(method) }.to raise_error(Virility::UnknownStrategy, "#{method} Is Not A Known Strategy") 82 | end 83 | end 84 | end 85 | 86 | context "strategy_exists?" do 87 | Virility::TESTING_STRATEGIES.keys.each do |strategy| 88 | it "should return true for #{strategy}" do 89 | expect(Virility::Excitation.new(@url).strategy_exists?(strategy)).to be(true) 90 | end 91 | end 92 | 93 | Virility::FAKE_TESTING_STRATEGIES.each do |strategy| 94 | it "should return false for #{strategy}" do 95 | expect(Virility::Excitation.new(@url).strategy_exists?(strategy)).to be(false) 96 | end 97 | end 98 | end 99 | 100 | context "get_strategy" do 101 | Virility::TESTING_STRATEGIES.each do |method, klass| 102 | it "should return a #{klass} object when get_strategy is called with #{method}" do 103 | expect(Virility::Excitation.new(@url).get_strategy(method)).to be_a_kind_of(klass) 104 | end 105 | end 106 | 107 | Virility::FAKE_TESTING_STRATEGIES.each do |method| 108 | it "should raise an error if the strategy (#{method}) does not exist" do 109 | expect(lambda { Virility::Excitation.new(@url).get_strategy(method) }).to raise_error(Virility::UnknownStrategy, "#{method} Is Not A Known Strategy") 110 | end 111 | end 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /spec/strategies/facebook_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | describe "Virility::Facebook" do 4 | before(:each) do 5 | @url = "http://creativeallies.com" 6 | end 7 | 8 | RSpec.shared_examples "no facebook results" do 9 | it "should not raise an error" do 10 | expect{ @virility.poll }.not_to raise_error 11 | end 12 | 13 | ["share_count", "comment_count", "engagement"].each do |attribute| 14 | it "should return 0 for #{attribute}" do 15 | expect(@virility.send(attribute.to_sym)).to eq(0) 16 | end 17 | end 18 | end 19 | 20 | describe "poll" do 21 | context "when there is not a valid result" do 22 | before(:each) do 23 | response = double("HTTParty::Response", :parsed_response => {"links_getStats_response"=>{"list"=>"true"}}) 24 | allow(Virility::Facebook).to receive(:get) { response } 25 | @virility = Virility::Facebook.new(@url) 26 | end 27 | 28 | it_should_behave_like "no facebook results" 29 | end 30 | 31 | context "when there is no result" do 32 | before(:each) do 33 | response = double("HTTParty::Response") 34 | allow(Virility::Facebook).to receive(:get) { response } 35 | @virility = Virility::Facebook.new(@url) 36 | end 37 | 38 | it_should_behave_like "no facebook results" 39 | end 40 | 41 | context "when there is a result but no response" do 42 | before(:each) do 43 | response = double("HTTParty::Response", :parsed_response => {}) 44 | allow(Virility::Facebook).to receive(:get) { response } 45 | @virility = Virility::Facebook.new(@url) 46 | end 47 | 48 | it_should_behave_like "no facebook results" 49 | end 50 | 51 | context "when there is a result but parsed_response is weird" do 52 | before(:each) do 53 | response = double("HTTParty::Response", :parsed_response => Object.new) 54 | allow(Virility::Facebook).to receive(:get) { response } 55 | @virility = Virility::Facebook.new(@url) 56 | end 57 | 58 | it_should_behave_like "no facebook results" 59 | end 60 | 61 | context "when there is a valid result" do 62 | let(:fb_response) { { 'share' => { 'comment_count' => '4', 'share_count' => '97173'}, 63 | 'og_object' => { 'engagement' => { 'count' => '97384', 'social_sentence' => "97K people like this."}, 64 | title: "Guardians of the Galaxy (2014)", id: "10150298925420108"}, id: "http://www.imdb.com/title/tt2015381/"} } 65 | before(:each) do 66 | response = double("HTTParty::Response", parsed_response: fb_response) 67 | allow(Virility::Facebook).to receive(:get) { response } 68 | @virility = Virility::Facebook.new(@url) 69 | end 70 | 71 | it "should not raise an error" do 72 | expect{ @virility.poll }.not_to raise_error 73 | end 74 | 75 | {"share_count"=>"97173", "engagement_count"=>'97384', "comment_count"=>"4", 'social_sentence' => "97K people like this."}.each do |key, value| 76 | it "should return #{value} for #{key}" do 77 | expect(@virility.send(key.to_sym)).to eq(value) 78 | end 79 | end 80 | end 81 | 82 | context "when there is a valid result, but not all fields are present" do 83 | let(:fb_response) { { 'share' => { 'comment_count' => '4', 'share_count' => '97173'}, 84 | 'og_object' => { 'engagement' => { 'count' => '97384', 'social_sentence' => "97K people like this."}, 85 | title: "Guardians of the Galaxy (2014)", id: "10150298925420108"}, id: "http://www.imdb.com/title/tt2015381/"} } 86 | before(:each) do 87 | response = double('HTTParty::Response', parsed_response: fb_response) 88 | allow(Virility::Facebook).to receive(:get) { response } 89 | @virility = Virility::Facebook.new(@url) 90 | end 91 | it "should not raise an error" do 92 | expect{ @virility.poll }.not_to raise_error 93 | end 94 | {"share_count"=>"97173", "engagement_count"=>'97384', "comment_count"=>"4", 'social_sentence' => "97K people like this."}.each do |key, value| 95 | it "should return #{value} for #{key}" do 96 | expect(@virility.send(key.to_sym)).to eq(value) 97 | end 98 | end 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /spec/virility_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/spec_helper') 2 | 3 | describe "Virility" do 4 | 5 | # 6 | # Factory 7 | # 8 | 9 | describe "factory" do 10 | context "valid strategies" do 11 | Virility::TESTING_STRATEGIES.each do |strategy, object| 12 | it "#{strategy} should create and return a #{object} object" do 13 | expect(Virility.factory(strategy, "http://creativeallies.com")).to be_a_kind_of(object) 14 | end 15 | end 16 | end 17 | 18 | context "invalid strategies" do 19 | Virility::FAKE_TESTING_STRATEGIES.each do |strategy| 20 | it "#{strategy} should raise an error" do 21 | expect{ Virility.factory(strategy, "http://creativeallies.com") }.to raise_error(Virility::UnknownStrategy, "#{strategy} Is Not A Known Strategy") 22 | end 23 | end 24 | end 25 | end 26 | 27 | # 28 | # Public API 29 | # 30 | 31 | describe "Public API testing" do 32 | before(:each) do 33 | @url = "http://creativeallies.com" 34 | allow(Virility::Facebook).to receive(:get) { double("HTTParty::Response", :parsed_response => { 'share' => { 'comment_count' => '4', 'share_count' => '97173'}, 'og_object' => { 'engagement' => { 'count' => '97384', 'social_sentence' => "97K people like this."}, title: "Guardians of the Galaxy (2014)", id: "10150298925420108"}, id: "http://www.imdb.com/title/tt2015381/"}) } 35 | allow(Virility::Pinterest).to receive(:get) { double("HTTParty::Response", :parsed_response => {"count"=>1, "url"=>"http://creativeallies.com"}) } 36 | allow(Virility::PlusOne).to receive(:get) { double("HTTParty::Response", :parsed_response => {"shares"=>"8"}) } 37 | allow(Virility::StumbleUpon).to receive(:get) { double("HTTParty::Response", :parsed_response => {"url"=>"http://creativeallies.com/", "in_index"=>true, "publicid"=>"2UhTwK", "views"=>4731, "title"=>"Creative Allies | Create Art For Rockstars | Upload For A Chance To Win", "thumbnail"=>"http://cdn.stumble-upon.com/mthumb/388/49348388.jpg", "thumbnail_b"=>"http://cdn.stumble-upon.com/images/nobthumb.png", "submit_link"=>"http://www.stumbleupon.com/submit/?url=http://creativeallies.com/", "badge_link"=>"http://www.stumbleupon.com/badge/?url=http://creativeallies.com/", "info_link"=>"http://www.stumbleupon.com/url/creativeallies.com/"}) } 38 | allow(Virility::Linkedin).to receive(:get) { double("HTTParty::Response", :parsed_response => { "count":17, "fCnt":"17", "fCntPlusOne":"18", "url":"http:\/\/creativeallies.com" }) } 39 | allow(Virility::Reddit).to receive(:get) { double("HTTParty::Response", :parsed_response => { "data" => { "children" => [{ "data" => { "domain" => "apple.com", "banned_by" => nil, "media_embed" => {}, "num_reports" => nil, "score" => 1}}, { "data" => { "domain" => "apple.com", "banned_by" => nil, "media_embed" => {}, "num_reports" => nil, "score" => 34 }}]}})} 40 | end 41 | 42 | it "Virility.counts should return a hash of counts" do 43 | expect(Virility.counts(@url)).to eq({:facebook=>97384, :linkedin => 17, :pinterest=>1, :plus_one=>8, reddit:35, :stumble_upon=>4731}) 44 | end 45 | 46 | it "Virility.total should return the total count" do 47 | expect(Virility.total(@url)).to eq(102176) 48 | end 49 | 50 | it "Virility.poll should return all of the hashed responses" do 51 | expect(Virility.poll(@url)).to eq({ 52 | :facebook=>{"comment_count"=>"4", "share_count"=>"97173", "engagement_count"=>"97384", "social_sentence"=>"97K people like this."}, 53 | :linkedin=>{ "count":17, "fCnt":"17", "fCntPlusOne":"18", "url":"http:\/\/creativeallies.com" }, 54 | :pinterest=>{"count"=>1, "url"=>"http://creativeallies.com"}, 55 | :plus_one=>{"shares"=>"8"}, 56 | :stumble_upon=>{"url"=>"http://creativeallies.com/", "in_index"=>true, "publicid"=>"2UhTwK", "views"=>4731, "title"=>"Creative Allies | Create Art For Rockstars | Upload For A Chance To Win", "thumbnail"=>"http://cdn.stumble-upon.com/mthumb/388/49348388.jpg", "thumbnail_b"=>"http://cdn.stumble-upon.com/images/nobthumb.png", "submit_link"=>"http://www.stumbleupon.com/submit/?url=http://creativeallies.com/", "badge_link"=>"http://www.stumbleupon.com/badge/?url=http://creativeallies.com/", "info_link"=>"http://www.stumbleupon.com/url/creativeallies.com/"}, 57 | :reddit=>{"score"=>35 } 58 | }) 59 | end 60 | it "Virility.poll should return all of the hashed responses with filtered strategies only" do 61 | expect(Virility.poll(@url,[:facebook,:linkedin,:pinterest,:plus_one,:stumble_upon])).to eq({ 62 | :facebook=>{"share_count"=>"97173", "engagement_count"=>'97384', "comment_count"=>"4", 'social_sentence' => "97K people like this."}, 63 | :linkedin=>{ "count":17, "fCnt":"17", "fCntPlusOne":"18", "url":"http:\/\/creativeallies.com" }, 64 | :pinterest=>{"count"=>1, "url"=>"http://creativeallies.com"}, 65 | :plus_one=>{"shares"=>"8"}, 66 | :stumble_upon=>{"url"=>"http://creativeallies.com/", "in_index"=>true, "publicid"=>"2UhTwK", "views"=>4731, "title"=>"Creative Allies | Create Art For Rockstars | Upload For A Chance To Win", "thumbnail"=>"http://cdn.stumble-upon.com/mthumb/388/49348388.jpg", "thumbnail_b"=>"http://cdn.stumble-upon.com/images/nobthumb.png", "submit_link"=>"http://www.stumbleupon.com/submit/?url=http://creativeallies.com/", "badge_link"=>"http://www.stumbleupon.com/badge/?url=http://creativeallies.com/", "info_link"=>"http://www.stumbleupon.com/url/creativeallies.com/"} 67 | }) 68 | end 69 | it "Virility.poll should return all of the hashed responses with filtered strategies only" do 70 | expect(Virility.poll(@url,[:facebook,:linkedin,:pinterest,:plus_one,:stumble_upon])).to eq({ 71 | :facebook=>{"share_count"=>"97173", "engagement_count"=>'97384', "comment_count"=>"4", 'social_sentence' => "97K people like this."}, 72 | :linkedin=>{ "count":17, "fCnt":"17", "fCntPlusOne":"18", "url":"http:\/\/creativeallies.com" }, 73 | :pinterest=>{"count"=>1, "url"=>"http://creativeallies.com"}, 74 | :plus_one=>{"shares"=>"8"}, 75 | :stumble_upon=>{"url"=>"http://creativeallies.com/", "in_index"=>true, "publicid"=>"2UhTwK", "views"=>4731, "title"=>"Creative Allies | Create Art For Rockstars | Upload For A Chance To Win", "thumbnail"=>"http://cdn.stumble-upon.com/mthumb/388/49348388.jpg", "thumbnail_b"=>"http://cdn.stumble-upon.com/images/nobthumb.png", "submit_link"=>"http://www.stumbleupon.com/submit/?url=http://creativeallies.com/", "badge_link"=>"http://www.stumbleupon.com/badge/?url=http://creativeallies.com/", "info_link"=>"http://www.stumbleupon.com/url/creativeallies.com/"} 76 | }) 77 | end 78 | 79 | it "Virility.url should return a Virility::Excitation object" do 80 | expect(Virility.url(@url)).to be_a_kind_of(Virility::Excitation) 81 | end 82 | end 83 | 84 | # 85 | # Error Proofing 86 | # 87 | 88 | describe "Error Proofing" do 89 | it "should not raise an error with a bad URL" do 90 | expect{ Virility.counts("http://this.is.a.crap.url") }.not_to raise_error 91 | end 92 | 93 | it "should return 0 for all strategy counts" do 94 | @virility = Virility.url("http://this.is.a.crap.url") 95 | expect(@virility.total).to eq(0) 96 | expect(@virility.counts).to eq({:facebook=>0, :linkedin=>0, :pinterest=>0, :plus_one=>0, :reddit=>0, :stumble_upon=>0 }) 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Virility 2 | 3 | Virility calls upon the API's of many popular social services such as Facebook, Reddit and Pinterest to collect the number of likes, tweets, pins etc. of a particular URL. Written with a modular construction, Virility makes it easy to drop new data collection strategies into the framework so that you can collect all of your statistics in one easy location. 4 | 5 | View a demo online: http://virility.herokuapp.com/ 6 | 7 | ## Installation 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | gem 'virility' 12 | 13 | And then execute: 14 | 15 | $ bundle 16 | 17 | Or install it yourself as: 18 | 19 | $ gem install virility 20 | 21 | ## Basic Usage 22 | 23 | If all you need is the raw shares numbers for a URL, Virility has some very simple methods you can use right out of the box: 24 | 25 | Virility.poll("http://rubygems.org") # => Returns a hash with the collected results from all of the social network strategies 26 | Virility.counts("http://rubygems.org") # => {:facebook=>72, :pinterest=>0, :plus_one=>138, :stumble_upon=>1488, :reddit=>2322, :linkedin => 7} 27 | Virility.total("http://rubygems.org") # => 4020 28 | Virility.url("http://rubygems.org") # => Returns a Virility::Excitation object that you can manipulate 29 | 30 | ## More Granular Usage 31 | 32 | The Virility::Excitation object does the heavy lifting of collecting the data from all of the available strategies. 33 | 34 | virility = Virility::Excitation.new("http://rubygems.org") 35 | virility.poll # returns a hash with the collected output of all data sources 36 | virility.counts # returns a hash of just the virility counts => {:facebook=>5116303, :linkedin => 17, :pinterest=>1, :plus_one=>8, reddit:35, :stumble_upon=>4731 } 37 | virility.total # returns the sum of all virility counts 38 | 39 | ## Individual Strategies 40 | 41 | Currently there is support for the following social resources: 42 | * Facebook 43 | * Linkedin 44 | * Pinterest 45 | * Google Plus One 46 | * Reddit 47 | * Stumble Upon 48 | 49 | Each social resource is implemented as a Virility::Strategy and contains at least three methods: poll, results and count. __poll__ does the work of querying the API to get the data and returns the same hash as the results method, __results__ returns the hashed values that were provided by the social network and __count__ pulls out the individual number of shares for that social network. 50 | 51 | #### Strategy Initialization 52 | 53 | There are several ways you can access the object for an individual strategy. 54 | 55 | The Virility object has a __factory__ method that will return the strategy object: 56 | 57 | reddit = Virility.factory(:reddit, "http://rubygems.org") # => returns a Virility::Reddit object with the rubygems url 58 | 59 | You can also instate the Virility::Reddit object directly: 60 | 61 | reddit = Virility::Reddit.new("http://rubygems.org") 62 | 63 | #### Ignoring Strategies 64 | 65 | Thanks to (Storyful)[https://github.com/storyful/virility], it is now possible to specify which strategies you want to use when initializing the Excitation object. Simply pass in an array of identifiers when creating the object and only those strategies will be implemented. The default is to use all available strategies. 66 | 67 | Virility.poll("http://rubygems.org",strategies: [:facebook,:linkedin,:pinterest]) 68 | 69 | #### Using Proxy Server 70 | 71 | It is now possible to specify the ip address of a proxy server to utilise whilst performing the call 72 | 73 | Virility.poll("http://rubygems.org", proxy: { http_proxyaddr: '192.168.0.23', http_proxyport: 8888 } ) 74 | 75 | #### Individual Usage Example 76 | 77 | Let's say you only need to get the number of tweets for a URL, you could use the Virility::Reddit class by itself: 78 | 79 | tweets = Virility::Reddit.new("http://rubygems.org") 80 | tweets.poll # returns a hash with the collected output from Reddit => {"url"=>"http://rubygems.org/", "count"=>2319} 81 | tweets.results # returns a hash with the collected output from Reddit => {"url"=>"http://rubygems.org/", "count"=>2319} 82 | tweets.count # returns the number of tweets for that URL => 2319 83 | 84 | ## Facebook Usage 85 | 86 | fb = Virility::Facebook.new("http://rubygems.org") 87 | fb.poll # returns a hash with the collected output from Facebook 88 | fb.count # returns the engagement_count for that URL 89 | 90 | The Facebook strategy leverages the Graph api call. Because of this, the following data fields are available: 91 | * comment_count 92 | * share_count 93 | * engagement_count 94 | * social_sentence 95 | 96 | However, the share_count and engagement_count return the same value for un-authenticated api calls. 97 | 98 | #### Virility::Excitation 99 | 100 | If you have a Virility::Excitation object, there are dynamic finders that will return the individual Virility::Strategy object for a social network. Simply call the name of the strategy against the Virility::Excitation object and that strategy will be returned: 101 | 102 | virility = Virility::Excitation.new("http://rubygems.org") 103 | facebook = virility.facebook 104 | linkedin = virility.linkedin 105 | reddit = virility.reddit 106 | pinterest = virility.pinterest 107 | plus_one = virility.plus_one 108 | stumble_upon = virility.stumble_upon 109 | 110 | #### Virility::Strategy 111 | 112 | If you have a Strategy object, any of the attributes that are commonly returned through the API call will be available as a dynamic finder. This is particularly useful with the Facebook strategy: 113 | 114 | fb = Virility::Facebook.new("http://rubygems.org/") 115 | fb.comment_count # => 0 116 | fb.share_count # => 673 117 | fb.engagement_count # => 673 118 | fb.social_sentence # => "673 people like this." 119 | 120 | #### Combined Finders 121 | 122 | Leveraging both sets of dynamic finders allows you to build an Excitation object and get all the way through to an attribute for a specific strategy: 123 | 124 | Virility.url("http://google.com/").facebook.share_count # => 39790003 125 | Virility.url("http://google.com/").stumble_upon.info_link # => "http://www.stumbleupon.com/url/www.google.com/" 126 | 127 | ## Important Notes 128 | 129 | URL's are very specific in the context of a social media. For example, http://rubygems.org will return different results than http://rubygems.org/ with a trailing slash. Also, http vs https will give you different numbers. This actually has a lot to do with why we created this gem. When testing be sure to investigate all of the URL variations in order to get the most complete picture of your data. 130 | 131 | #### Case Study 132 | 133 | Compare the total count results for http://ruby-lang.org/en. One has the trailing slash and one does not. 134 | 135 | Virility::Excitation.new("http://www.ruby-lang.org/en").total # => 247695 136 | Virility::Excitation.new("http://www.ruby-lang.org/en/").total # => 253190 137 | 138 | On this particular day, there was a 5,495 count difference between the two values. Inspecting the actual results shows you which of the social networks takes the varying forms of the urls into account: 139 | 140 | Virility::Excitation.new("http://www.ruby-lang.org/en").counts 141 | # => {:delicious=>37, :facebook=>3, :pinterest=>0, :plusone=>20, :stumbleupon=>246937} 142 | 143 | Virility::Excitation.new("http://www.ruby-lang.org/en/").counts 144 | # => {:delicious=>4314, :facebook=>813, :pinterest=>22, :plusone=>406, :stumbleupon=>246937} 145 | 146 | Stumbleupon is consistent while Facebook, Pinterest and Google Plus One return different results. Depending on your needs, you could craft an algorithm that takes all of this into account and attempts to deliver an accurate number by combining the data sets that are different and trusting the ones that are the same. 147 | 148 | Based on this logic, it is possible to consider that the true total share count is closer to _253,250_. Not only is this an opinionated number, it's accuracy is questionable based on assumptions, however if you are just trying to get a ballpark feeling of the virility of your content, this number should suffice. 149 | 150 | ## Contributing 151 | 152 | 1. Fork it 153 | 2. Create your feature branch (`git checkout -b my-new-feature`) 154 | 3. Commit your changes (`git commit -am 'Add some feature'`) 155 | 4. Push to the branch (`git push origin my-new-feature`) 156 | 5. Create new Pull Request 157 | 158 | ## Copyright 159 | 160 | Copyright (c) 2016 Jay Sanders. See LICENSE.txt for 161 | further details. 162 | --------------------------------------------------------------------------------