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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +481 -0
- data/Rakefile +10 -0
- data/bin/console +42 -0
- data/birdwatcher.gemspec +40 -0
- data/data/english_stopwords.txt +319 -0
- data/data/top100Kenglishwords.txt +100000 -0
- data/db/migrations/001_create_workspaces.rb +11 -0
- data/db/migrations/002_create_users.rb +29 -0
- data/db/migrations/003_create_statuses.rb +28 -0
- data/db/migrations/004_create_mentions.rb +13 -0
- data/db/migrations/005_create_mentions_statuses.rb +8 -0
- data/db/migrations/006_create_hashtags.rb +11 -0
- data/db/migrations/007_create_hashtags_statuses.rb +8 -0
- data/db/migrations/008_create_urls.rb +16 -0
- data/db/migrations/009_create_statuses_urls.rb +8 -0
- data/db/migrations/010_create_klout_topics.rb +10 -0
- data/db/migrations/011_create_klout_topics_users.rb +8 -0
- data/db/migrations/012_create_influencers.rb +10 -0
- data/db/migrations/013_create_influencers_users.rb +8 -0
- data/db/migrations/014_create_influencees.rb +10 -0
- data/db/migrations/015_create_influencees_users.rb +8 -0
- data/exe/birdwatcher +12 -0
- data/lib/birdwatcher/command.rb +78 -0
- data/lib/birdwatcher/commands/back.rb +15 -0
- data/lib/birdwatcher/commands/exit.rb +16 -0
- data/lib/birdwatcher/commands/help.rb +60 -0
- data/lib/birdwatcher/commands/irb.rb +34 -0
- data/lib/birdwatcher/commands/module.rb +106 -0
- data/lib/birdwatcher/commands/query.rb +58 -0
- data/lib/birdwatcher/commands/query_csv.rb +56 -0
- data/lib/birdwatcher/commands/resource.rb +45 -0
- data/lib/birdwatcher/commands/run.rb +19 -0
- data/lib/birdwatcher/commands/schema.rb +116 -0
- data/lib/birdwatcher/commands/set.rb +56 -0
- data/lib/birdwatcher/commands/shell.rb +21 -0
- data/lib/birdwatcher/commands/show.rb +86 -0
- data/lib/birdwatcher/commands/status.rb +114 -0
- data/lib/birdwatcher/commands/unset.rb +37 -0
- data/lib/birdwatcher/commands/use.rb +25 -0
- data/lib/birdwatcher/commands/user.rb +155 -0
- data/lib/birdwatcher/commands/workspace.rb +176 -0
- data/lib/birdwatcher/concerns/concurrency.rb +25 -0
- data/lib/birdwatcher/concerns/core.rb +105 -0
- data/lib/birdwatcher/concerns/outputting.rb +114 -0
- data/lib/birdwatcher/concerns/persistence.rb +101 -0
- data/lib/birdwatcher/concerns/presentation.rb +122 -0
- data/lib/birdwatcher/concerns/util.rb +138 -0
- data/lib/birdwatcher/configuration.rb +63 -0
- data/lib/birdwatcher/configuration_wizard.rb +65 -0
- data/lib/birdwatcher/console.rb +201 -0
- data/lib/birdwatcher/http_client.rb +164 -0
- data/lib/birdwatcher/klout_client.rb +83 -0
- data/lib/birdwatcher/kml.rb +125 -0
- data/lib/birdwatcher/module.rb +253 -0
- data/lib/birdwatcher/modules/statuses/kml.rb +106 -0
- data/lib/birdwatcher/modules/statuses/sentiment.rb +77 -0
- data/lib/birdwatcher/modules/statuses/word_cloud.rb +205 -0
- data/lib/birdwatcher/modules/urls/crawl.rb +138 -0
- data/lib/birdwatcher/modules/urls/most_shared.rb +98 -0
- data/lib/birdwatcher/modules/users/activity_plot.rb +62 -0
- data/lib/birdwatcher/modules/users/import.rb +61 -0
- data/lib/birdwatcher/modules/users/influence_graph.rb +93 -0
- data/lib/birdwatcher/modules/users/klout_id.rb +62 -0
- data/lib/birdwatcher/modules/users/klout_influence.rb +83 -0
- data/lib/birdwatcher/modules/users/klout_score.rb +64 -0
- data/lib/birdwatcher/modules/users/klout_topics.rb +72 -0
- data/lib/birdwatcher/modules/users/social_graph.rb +110 -0
- data/lib/birdwatcher/punchcard.rb +183 -0
- data/lib/birdwatcher/util.rb +83 -0
- data/lib/birdwatcher/version.rb +3 -0
- data/lib/birdwatcher.rb +43 -0
- data/models/hashtag.rb +8 -0
- data/models/influencee.rb +8 -0
- data/models/influencer.rb +8 -0
- data/models/klout_topic.rb +8 -0
- data/models/mention.rb +8 -0
- data/models/status.rb +11 -0
- data/models/url.rb +8 -0
- data/models/user.rb +11 -0
- data/models/workspace.rb +26 -0
- 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
|