birdwatcher 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.travis.yml +5 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +481 -0
  7. data/Rakefile +10 -0
  8. data/bin/console +42 -0
  9. data/birdwatcher.gemspec +40 -0
  10. data/data/english_stopwords.txt +319 -0
  11. data/data/top100Kenglishwords.txt +100000 -0
  12. data/db/migrations/001_create_workspaces.rb +11 -0
  13. data/db/migrations/002_create_users.rb +29 -0
  14. data/db/migrations/003_create_statuses.rb +28 -0
  15. data/db/migrations/004_create_mentions.rb +13 -0
  16. data/db/migrations/005_create_mentions_statuses.rb +8 -0
  17. data/db/migrations/006_create_hashtags.rb +11 -0
  18. data/db/migrations/007_create_hashtags_statuses.rb +8 -0
  19. data/db/migrations/008_create_urls.rb +16 -0
  20. data/db/migrations/009_create_statuses_urls.rb +8 -0
  21. data/db/migrations/010_create_klout_topics.rb +10 -0
  22. data/db/migrations/011_create_klout_topics_users.rb +8 -0
  23. data/db/migrations/012_create_influencers.rb +10 -0
  24. data/db/migrations/013_create_influencers_users.rb +8 -0
  25. data/db/migrations/014_create_influencees.rb +10 -0
  26. data/db/migrations/015_create_influencees_users.rb +8 -0
  27. data/exe/birdwatcher +12 -0
  28. data/lib/birdwatcher/command.rb +78 -0
  29. data/lib/birdwatcher/commands/back.rb +15 -0
  30. data/lib/birdwatcher/commands/exit.rb +16 -0
  31. data/lib/birdwatcher/commands/help.rb +60 -0
  32. data/lib/birdwatcher/commands/irb.rb +34 -0
  33. data/lib/birdwatcher/commands/module.rb +106 -0
  34. data/lib/birdwatcher/commands/query.rb +58 -0
  35. data/lib/birdwatcher/commands/query_csv.rb +56 -0
  36. data/lib/birdwatcher/commands/resource.rb +45 -0
  37. data/lib/birdwatcher/commands/run.rb +19 -0
  38. data/lib/birdwatcher/commands/schema.rb +116 -0
  39. data/lib/birdwatcher/commands/set.rb +56 -0
  40. data/lib/birdwatcher/commands/shell.rb +21 -0
  41. data/lib/birdwatcher/commands/show.rb +86 -0
  42. data/lib/birdwatcher/commands/status.rb +114 -0
  43. data/lib/birdwatcher/commands/unset.rb +37 -0
  44. data/lib/birdwatcher/commands/use.rb +25 -0
  45. data/lib/birdwatcher/commands/user.rb +155 -0
  46. data/lib/birdwatcher/commands/workspace.rb +176 -0
  47. data/lib/birdwatcher/concerns/concurrency.rb +25 -0
  48. data/lib/birdwatcher/concerns/core.rb +105 -0
  49. data/lib/birdwatcher/concerns/outputting.rb +114 -0
  50. data/lib/birdwatcher/concerns/persistence.rb +101 -0
  51. data/lib/birdwatcher/concerns/presentation.rb +122 -0
  52. data/lib/birdwatcher/concerns/util.rb +138 -0
  53. data/lib/birdwatcher/configuration.rb +63 -0
  54. data/lib/birdwatcher/configuration_wizard.rb +65 -0
  55. data/lib/birdwatcher/console.rb +201 -0
  56. data/lib/birdwatcher/http_client.rb +164 -0
  57. data/lib/birdwatcher/klout_client.rb +83 -0
  58. data/lib/birdwatcher/kml.rb +125 -0
  59. data/lib/birdwatcher/module.rb +253 -0
  60. data/lib/birdwatcher/modules/statuses/kml.rb +106 -0
  61. data/lib/birdwatcher/modules/statuses/sentiment.rb +77 -0
  62. data/lib/birdwatcher/modules/statuses/word_cloud.rb +205 -0
  63. data/lib/birdwatcher/modules/urls/crawl.rb +138 -0
  64. data/lib/birdwatcher/modules/urls/most_shared.rb +98 -0
  65. data/lib/birdwatcher/modules/users/activity_plot.rb +62 -0
  66. data/lib/birdwatcher/modules/users/import.rb +61 -0
  67. data/lib/birdwatcher/modules/users/influence_graph.rb +93 -0
  68. data/lib/birdwatcher/modules/users/klout_id.rb +62 -0
  69. data/lib/birdwatcher/modules/users/klout_influence.rb +83 -0
  70. data/lib/birdwatcher/modules/users/klout_score.rb +64 -0
  71. data/lib/birdwatcher/modules/users/klout_topics.rb +72 -0
  72. data/lib/birdwatcher/modules/users/social_graph.rb +110 -0
  73. data/lib/birdwatcher/punchcard.rb +183 -0
  74. data/lib/birdwatcher/util.rb +83 -0
  75. data/lib/birdwatcher/version.rb +3 -0
  76. data/lib/birdwatcher.rb +43 -0
  77. data/models/hashtag.rb +8 -0
  78. data/models/influencee.rb +8 -0
  79. data/models/influencer.rb +8 -0
  80. data/models/klout_topic.rb +8 -0
  81. data/models/mention.rb +8 -0
  82. data/models/status.rb +11 -0
  83. data/models/url.rb +8 -0
  84. data/models/user.rb +11 -0
  85. data/models/workspace.rb +26 -0
  86. metadata +405 -0
@@ -0,0 +1,138 @@
1
+ module Birdwatcher
2
+ module Concerns
3
+ module Util
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ module ClassMethods
9
+ end
10
+
11
+ # Get the relative time for a timestamp
12
+ #
13
+ # @param time [Time] Timestamp to be converted
14
+ #
15
+ # @example getting relative time of a status
16
+ # status = current_workspace.statuses.last
17
+ # output time_ago_in_words(status.posted_at)
18
+ # #=> "1 day and 15 hours ago"
19
+ #
20
+ # @return [String] relative time in words
21
+ def time_ago_in_words(time)
22
+ Birdwatcher::Util.time_ago_in_words(time)
23
+ end
24
+
25
+ # Natural language parsing of time
26
+ #
27
+ # @param time [String] A string representing a time (e.g. "yesterday at 4:00")
28
+ #
29
+ # Uses the Chronic gem to perform natural language parsing of time.
30
+ # See the examples in Chronic's documentation for strings that can be parsed.
31
+ #
32
+ # All modules that can be configured with times, should perform natural
33
+ # language parsing on the option setting for better user experience.
34
+ #
35
+ # @return [Time]
36
+ # @see https://github.com/mojombo/chronic
37
+ # @see https://github.com/mojombo/chronic#examples
38
+ def parse_time(time)
39
+ Birdwatcher::Util.parse_time(time)
40
+ end
41
+
42
+ # Correct pluralization of word depending on count
43
+ #
44
+ # @param count [Integer] The amount
45
+ # @param singular [String] The singular word
46
+ # @param plural [String] The plural word
47
+ #
48
+ # pluralizes the singular word unless count is 1.
49
+ #
50
+ # @example
51
+ # pluralize(1, "user", "users")
52
+ # #=> "1 user"
53
+ #
54
+ # pluralize(5, "user", "users")
55
+ # #=> "5 users"
56
+ #
57
+ # pluralize(0, "user", "users")
58
+ # #=> "0 users"
59
+ def pluralize(count, singular, plural)
60
+ Birdwatcher::Util.pluralize(count, singular, plural)
61
+ end
62
+
63
+ # Strip out HTML tags from a string
64
+ #
65
+ # @param string [String] String to strip HTML tags from
66
+ #
67
+ # @return [String] HTML stripped string
68
+ def strip_html(string)
69
+ Birdwatcher::Util.strip_html(string)
70
+ end
71
+
72
+ # Strip out control characters and color codes from a string
73
+ #
74
+ # @param string [String] String to strip control characters from
75
+ #
76
+ # @return [String] String without control characters
77
+ def strip_control_characters(string)
78
+ Birdwatcher::Util.strip_control_characters(string)
79
+ end
80
+
81
+ # Escape special HTML characters with HTML entities
82
+ #
83
+ # @param string [String] String to HTML escape
84
+ #
85
+ # @return [String] HTML escaped string
86
+ def escape_html(string)
87
+ Birdwatcher::Util.escape_html(string)
88
+ end
89
+
90
+ # Unescape special HTML characters in a string
91
+ #
92
+ # @param string [String] String to unescape
93
+ #
94
+ # @return [String] string with escaped special HTML characters unescaped
95
+ def unescape_html(string)
96
+ Birdwatcher::Util.unescape_html(string)
97
+ end
98
+
99
+ # Create an excerpt of potentially long text at a fixed length
100
+ #
101
+ # @param text [String] Text to excerpt
102
+ # @param max_length [Integer] Maximum length of text before being excerpted
103
+ # @param omission [String] OPTIONAL: String to append to text if excerpted
104
+ #
105
+ # @example
106
+ # excerpt("The quick brown fox jumps over the lazy dog", 80)
107
+ # #=> "The quick brown fox jumps over the lazy dog"
108
+ #
109
+ # excerpt("The quick brown fox jumps over the lazy dog", 40)
110
+ # #=> "The quick brown fox jumps over the lazy d..."
111
+ #
112
+ # @return [String] excerpted text
113
+ def excerpt(text, max_length, omission = "...")
114
+ Birdwatcher::Util.excerpt(text, max_length, omission)
115
+ end
116
+
117
+ # Suppress any potential output to STDOUT
118
+ #
119
+ # Used in cases where certain libraries or methods might output unwanted
120
+ # text to +STDOUT+ without any possibility of disabling it.
121
+ #
122
+ # @param block code block to run with output suppression
123
+ def suppress_output(&block)
124
+ Birdwatcher::Util.suppress_output(&block)
125
+ end
126
+
127
+ # Suppress any warning messages to STDOUT
128
+ #
129
+ # Used in cases where certain libraries or methods might output unwanted
130
+ # warning messages to +STDOUT+.
131
+ #
132
+ # @param block code block to run with warning suppression
133
+ def suppress_warnings(&block)
134
+ Birdwatcher::Util.suppress_warnings(&block)
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,63 @@
1
+ module Birdwatcher
2
+ class Configuration
3
+ include Singleton
4
+
5
+ CONFIGURATION_FILE_NAME = ".birdwatcherrc".freeze
6
+ CONFIGURATION_FILE_LOCATION = File.join(Dir.home, CONFIGURATION_FILE_NAME).freeze
7
+
8
+ class Error < StandardError; end
9
+ class ConfigurationFileNotFound < Error; end
10
+ class ConfigurationFileNotReadable < Error; end
11
+ class ConfigurationFileCorrupt < Error; end
12
+ class UnknownKey < Error; end
13
+
14
+ def self.get!(key)
15
+ self.instance.get!(key)
16
+ end
17
+
18
+ def self.get(key)
19
+ self.instance.get!(key)
20
+ rescue Birdwatcher::Configuration::UnknownKey
21
+ nil
22
+ end
23
+
24
+ def self.configured?
25
+ File.exist?(CONFIGURATION_FILE_LOCATION)
26
+ end
27
+
28
+ def self.save!(configuration)
29
+ File.open(CONFIGURATION_FILE_LOCATION, "w") do |f|
30
+ f.write(YAML.dump(configuration))
31
+ end
32
+ end
33
+
34
+ def self.load!
35
+ self.instance.load_configuration!
36
+ end
37
+
38
+ def load_configuration!
39
+ if !File.exist?(CONFIGURATION_FILE_LOCATION)
40
+ fail ConfigurationFileNotFound, "Configuration file does not exist"
41
+ end
42
+ if !File.readable?(CONFIGURATION_FILE_LOCATION)
43
+ fail ConfigurationFileNotReadable, "Configuration file is not readable"
44
+ end
45
+ @configuration = YAML.load_file(CONFIGURATION_FILE_LOCATION).inject({}) { |memo, (k,v)| memo[k.to_sym] = v; memo }
46
+ rescue ::Psych::SyntaxError => e
47
+ raise ConfigurationFileCorrupt, "Configuration file contains invalid YAML"
48
+ end
49
+
50
+ def get!(key)
51
+ key = key.to_sym
52
+ fail(UnknownKey, "Unknown configuration key: #{key}") unless configuration.key?(key)
53
+ configuration[key.to_sym]
54
+ end
55
+
56
+ private
57
+
58
+ def configuration
59
+ load_configuration! unless @configuration
60
+ @configuration
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,65 @@
1
+ module Birdwatcher
2
+ class ConfigurationWizard
3
+ def start!
4
+ configuration = gather_configuration
5
+ end
6
+
7
+ private
8
+
9
+ def gather_configuration
10
+ Birdwatcher::Console.instance.info("Starting configuration wizard.\n")
11
+ configuration = {
12
+ "database_connection_uri" => gather_database_connection_uri,
13
+ "twitter" => gather_twitter_keys,
14
+ "klout" => gather_klout_keys
15
+ }
16
+ Birdwatcher::Configuration.save!(configuration)
17
+ Birdwatcher::Console.instance.newline
18
+ Birdwatcher::Console.instance.info("Configuration saved to #{Birdwatcher::Configuration::CONFIGURATION_FILE_LOCATION.bold}")
19
+ end
20
+
21
+ def gather_database_connection_uri
22
+ hostname = HighLine.ask("Enter PostgreSQL hostname: ") do |q|
23
+ q.default = "localhost"
24
+ end
25
+ port = HighLine.ask("Enter PostgreSQL port: |5432| ", Integer) do |q|
26
+ q.default = 5432
27
+ q.in = 1..65_535
28
+ end
29
+ username = HighLine.ask("Enter PostgreSQL username: ") do |q|
30
+ q.default = "birdwatcher"
31
+ end
32
+ password = HighLine.ask("Enter PostgreSQL password (masked): ") do |q|
33
+ q.echo = "x"
34
+ end
35
+ database = HighLine.ask("Enter PostgreSQL database name: ") do |q|
36
+ q.default = "birdwatcher"
37
+ end
38
+ "postgres://#{username}:#{password}@#{hostname}:#{port}/#{database}"
39
+ end
40
+
41
+ def gather_twitter_keys
42
+ keys = []
43
+ begin
44
+ consumer_key = HighLine.ask("Enter Twitter consumer key: ")
45
+ consumer_secret = HighLine.ask("Enter Twitter consumer secret: ")
46
+ keys << {
47
+ "consumer_key" => consumer_key,
48
+ "consumer_secret" => consumer_secret
49
+ }
50
+ end while HighLine.agree("Enter another Twitter keypair? (y/n) ")
51
+ keys
52
+ end
53
+
54
+ def gather_klout_keys
55
+ keys = []
56
+ puts "\nKlout access tokens can be used by modules to gather additional information on Twitter users."
57
+ if HighLine.agree("Do you want to enter Klout access tokens? (y/n) ")
58
+ begin
59
+ keys << HighLine.ask("Enter Klout access token: ")
60
+ end while HighLine.agree("Enter another Klout access token? (y/n) ")
61
+ end
62
+ keys
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,201 @@
1
+ module Birdwatcher
2
+ class Console
3
+ include Singleton
4
+
5
+ DEFAULT_AUTO_COMPLETION_STRINGS = [].freeze
6
+ DB_MIGRATIONS_PATH = File.expand_path("../../../db/migrations", __FILE__).freeze
7
+ LINE_SEPARATOR = ("=" * 80).freeze
8
+
9
+ attr_accessor :current_workspace, :current_module
10
+ attr_reader :database
11
+
12
+ def initialize
13
+ @output_mutex = Mutex.new
14
+ end
15
+
16
+ def start!
17
+ print_banner
18
+ bootstrap!
19
+ Readline.completion_proc = proc do |s|
20
+ expanded_s = File.expand_path(s)
21
+ Birdwatcher::Console.instance.auto_completion_strings.grep(/\A#{Regexp.escape(s)}/) + Dir["#{expanded_s}*"].grep(/^#{Regexp.escape(expanded_s)}/)
22
+ end
23
+ Readline.completion_append_character = ""
24
+ while input = Readline.readline(prompt_line, true)
25
+ input = input.to_s.strip
26
+ handle_input(input) unless input.empty?
27
+ end
28
+ end
29
+
30
+ def handle_input(input)
31
+ input.strip!
32
+ command_name, argument_line = input.split(" ", 2).map(&:strip)
33
+ command_name.downcase
34
+ commands.each do |command|
35
+ next unless command.has_name?(command_name)
36
+ command.new.execute(argument_line)
37
+ return true
38
+ end
39
+ error("Unknown command: #{command_name.bold}")
40
+ false
41
+ end
42
+
43
+ def auto_completion_strings
44
+ if !@auto_completion_strings
45
+ @auto_completion_strings = DEFAULT_AUTO_COMPLETION_STRINGS
46
+ commands.each { |c| @auto_completion_strings += c.auto_completion_strings }
47
+ @auto_completion_strings += Birdwatcher::Module.module_paths
48
+ end
49
+ @auto_completion_strings
50
+ end
51
+
52
+ def output(data, newline = true)
53
+ data = "#{data}\n" if newline
54
+ with_output_mutex { print data }
55
+ end
56
+
57
+ def output_formatted(*args)
58
+ with_output_mutex { printf(*args) }
59
+ end
60
+
61
+ def newline
62
+ with_output_mutex { puts }
63
+ end
64
+
65
+ def line_separator
66
+ output LINE_SEPARATOR
67
+ end
68
+
69
+ def info(message)
70
+ output "[+] ".bold.light_blue + message
71
+ end
72
+
73
+ def task(message, fatal = false, &block)
74
+ output("[+] ".bold.light_blue + message, false)
75
+ yield block
76
+ output " done".bold.light_green
77
+ rescue => e
78
+ output " failed".bold.light_red
79
+ error "#{e.class}: ".bold + e.message
80
+ exit(1) if fatal
81
+ end
82
+
83
+ def error(message)
84
+ output "[-] ".bold.light_red + message
85
+ end
86
+
87
+ def warn(message)
88
+ output "[!] ".bold.light_yellow + message
89
+ end
90
+
91
+ def fatal(message)
92
+ output "[-]".white.bold.on_red + " #{message}"
93
+ end
94
+
95
+ def twitter_client
96
+ if !@twitter_clients
97
+ @twitter_clients = create_twitter_clients!
98
+ end
99
+ @twitter_clients.sample
100
+ end
101
+
102
+ def klout_client
103
+ if !@klout_clients
104
+ @klout_clients = create_klout_clients!
105
+ end
106
+ @klout_clients.sample
107
+ end
108
+
109
+ private
110
+
111
+ def print_banner
112
+ output " ___ _ _ _ _\n" \
113
+ "| _ |_)_ _ __| |_ __ ____ _| |_ __| |_ ___ _ _\n" \
114
+ "| _ \\ | '_/ _` \\ V V / _` | _/ _| ' \\/ -_) '_|\n" \
115
+ "|___/_|_| \\__,_|\\_/\\_/\\__,_|\\__\\__|_||_\\___|_|\n".bold.light_blue +
116
+ " v#{Birdwatcher::VERSION} by " + "@michenriksen\n".bold
117
+ end
118
+
119
+ def bootstrap!
120
+ bootstrap_configuration!
121
+ bootstrap_database!
122
+ newline
123
+ end
124
+
125
+ def bootstrap_configuration!
126
+ if !Birdwatcher::Configuration.configured?
127
+ Birdwatcher::ConfigurationWizard.new.start!
128
+ end
129
+ task "Loading configuration...", true do
130
+ Birdwatcher::Configuration.load!
131
+ end
132
+ end
133
+
134
+ def bootstrap_database!
135
+ task "Preparing database...", true do
136
+ Sequel.extension :migration, :core_extensions
137
+ @database = Sequel.connect(configuration.get!(:database_connection_uri))
138
+ Sequel::Migrator.run(@database, DB_MIGRATIONS_PATH)
139
+ Sequel::Model.db = @database
140
+ Sequel::Model.plugin :timestamps
141
+ bootstrap_models!
142
+ load_default_workspace!
143
+ end
144
+ end
145
+
146
+ def bootstrap_models!
147
+ Dir[File.join(File.dirname(__FILE__), "..", "..", "models", "*.rb")].each do |file|
148
+ require file
149
+ end
150
+ end
151
+
152
+ def prompt_line
153
+ prompt = "birdwatcher[".bold + "#{current_workspace.name}" + "]".bold
154
+ if current_module
155
+ prompt += "[".bold + current_module.path.light_red + "]> ".bold
156
+ else
157
+ prompt += "> ".bold
158
+ end
159
+ prompt
160
+ end
161
+
162
+ def load_default_workspace!
163
+ @current_workspace = Birdwatcher::Models::Workspace.find_or_create(
164
+ :name => Birdwatcher::Models::Workspace::DEFAULT_WORKSPACE_NAME
165
+ ) do |w|
166
+ w.description = Birdwatcher::Models::Workspace::DEFAULT_WORKSPACE_DESCRIPTION
167
+ end
168
+ end
169
+
170
+ def commands
171
+ @commands ||= Birdwatcher::Command.descendants
172
+ end
173
+
174
+ def configuration
175
+ Birdwatcher::Configuration
176
+ end
177
+
178
+ def with_output_mutex
179
+ @output_mutex.synchronize { yield }
180
+ end
181
+
182
+ def create_twitter_clients!
183
+ clients = []
184
+ configuration.get!(:twitter).each do |keypair|
185
+ clients << Twitter::REST::Client.new do |config|
186
+ config.consumer_key = keypair["consumer_key"]
187
+ config.consumer_secret = keypair["consumer_secret"]
188
+ end
189
+ end
190
+ clients
191
+ end
192
+
193
+ def create_klout_clients!
194
+ clients = []
195
+ configuration.get(:klout).each do |key|
196
+ clients << Birdwatcher::KloutClient.new(key)
197
+ end
198
+ clients
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,164 @@
1
+ module Birdwatcher
2
+ class HttpClient
3
+ include HTTParty
4
+
5
+ # Default request timeout
6
+ DEFAULT_TIMEOUT = 15.freeze
7
+
8
+ # Default request retries
9
+ DEFAULT_RETRIES = 2.freeze
10
+
11
+ Response = Struct.new(:url, :status, :headers, :body)
12
+
13
+ # List of retriable exceptions
14
+ RETRIABLE_EXCEPTIONS = [
15
+ Errno::ETIMEDOUT,
16
+ Errno::ECONNRESET,
17
+ Errno::ECONNREFUSED,
18
+ Errno::ENETUNREACH,
19
+ Errno::EHOSTUNREACH,
20
+ Errno::EINVAL,
21
+ SocketError,
22
+ Net::OpenTimeout,
23
+ EOFError
24
+ ].freeze
25
+
26
+ # List of User-Agent strings to use for client spoofing
27
+ USER_AGENTS = [
28
+ "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
29
+ "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
30
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
31
+ "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36",
32
+ "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36",
33
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36",
34
+ "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:48.0) Gecko/20100101 Firefox/48.0",
35
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36",
36
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/601.7.8 (KHTML, like Gecko) Version/9.1.3 Safari/601.7.8",
37
+ "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:48.0) Gecko/20100101 Firefox/48.0",
38
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
39
+ "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko",
40
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:48.0) Gecko/20100101 Firefox/48.0",
41
+ "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:48.0) Gecko/20100101 Firefox/48.0",
42
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12) AppleWebKit/602.1.50 (KHTML, like Gecko) Version/10.0 Safari/602.1.50",
43
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/601.7.7 (KHTML, like Gecko) Version/9.1.2 Safari/601.7.7",
44
+ "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
45
+ "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36",
46
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
47
+ "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36"
48
+ ].freeze
49
+
50
+ # Class initializer
51
+ #
52
+ # @params options [Hash] client options
53
+ # @option options :timeout [Integer] request timeout
54
+ # @option options :retries [Integer] request retries
55
+ # @option options :user_agent [String] User-Agent to use (random if not set)
56
+ # @option options :retriable_exceptions [Array] exception classes to retry on
57
+ #
58
+ # @note The options list is incomplete; the class supports all options for the HTTParty gem
59
+ # @see http://www.rubydoc.info/github/jnunemaker/httparty/HTTParty/ClassMethods
60
+ def initialize(options = {})
61
+ @options = {
62
+ :timeout => DEFAULT_TIMEOUT,
63
+ :retries => DEFAULT_RETRIES,
64
+ :user_agent => nil,
65
+ :retriable_exceptions => RETRIABLE_EXCEPTIONS
66
+ }.merge(options)
67
+ end
68
+
69
+ # Perform a GET request
70
+ #
71
+ # @param path [String] Path to request
72
+ # @param params [Hash] Request params
73
+ # @param options [Hash] Request options
74
+ # @return [Birdwatcher::HttpClient::Response]
75
+ def do_get(path, params=nil, options={})
76
+ do_request(:get, path, {:query => params}.merge(options))
77
+ end
78
+
79
+ # Perform a HEAD request
80
+ #
81
+ # @param path [String] Path to request
82
+ # @param params [Hash] Request params
83
+ # @param options [Hash] Request options
84
+ # @return [Birdwatcher::HttpClient::Response]
85
+ def do_head(path, params=nil, options={})
86
+ do_request(:head, path, {:query => params}.merge(options))
87
+ end
88
+
89
+ # Perform a POST request
90
+ #
91
+ # @param path [String] Path to request
92
+ # @param params [Hash] Request params
93
+ # @param options [Hash] Request options
94
+ # @return [Birdwatcher::HttpClient::Response]
95
+ def do_post(path, params=nil, options={})
96
+ do_request(:post, path, {:query => params}.merge(options))
97
+ end
98
+
99
+ # Perform a PUT request
100
+ #
101
+ # @param path [String] Path to request
102
+ # @param params [Hash] Request params
103
+ # @param options [Hash] Request options
104
+ # @return [Birdwatcher::HttpClient::Response]
105
+ def do_put(path, params=nil, options={})
106
+ do_request(:put, path, {:query => params}.merge(options))
107
+ end
108
+
109
+ # Perform a DELETE request
110
+ #
111
+ # @param path [String] Path to request
112
+ # @param params [Hash] Request params
113
+ # @param options [Hash] Request options
114
+ # @return [Birdwatcher::HttpClient::Response]
115
+ def do_delete(path, params=nil, options={})
116
+ do_request(:delete, path, {:query => params}.merge(options))
117
+ end
118
+
119
+ private
120
+
121
+ # Perform a request
122
+ # @private
123
+ #
124
+ # @param method [Symbol] Class method to call
125
+ # @param path [String] Request path
126
+ # @param options [Hash] Request options
127
+ #
128
+ # @return Birdwatcher::HttpClient::Response
129
+ def do_request(method, path, options)
130
+ opts = @options.merge({
131
+ :headers => {
132
+ "Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
133
+ "Accept-Encoding" => "gzip",
134
+ "Accept-Language" => "en-US,en;q=0.8",
135
+ "Cache-Control" => "max-age=0",
136
+ "Connection" => "close",
137
+ "User-Agent" => @options[:user_agent] || USER_AGENTS.sample
138
+ },
139
+ :verify => false
140
+ }).merge(options)
141
+ with_retries do
142
+ response = self.class.send(method, path, opts)
143
+ Response.new(response.request.last_uri.to_s, response.code, response.headers, response.body)
144
+ end
145
+ end
146
+
147
+ # Execute code block and retry if a retriable exception is raised
148
+ # @private
149
+ #
150
+ # @param &block Code block to run
151
+ def with_retries(&block)
152
+ tries = @options[:retries].to_i
153
+ yield
154
+ rescue *@options[:retriable_exceptions] => ex
155
+ tries -= 1
156
+ if tries > 0
157
+ sleep 0.2
158
+ retry
159
+ else
160
+ raise ex
161
+ end
162
+ end
163
+ end
164
+ end