birdgrinder 0.1.0.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.
- data/bin/birdgrinder +34 -0
- data/examples/bird_grinder_client.rb +45 -0
- data/lib/bird_grinder/base.rb +134 -0
- data/lib/bird_grinder/cacheable.rb +66 -0
- data/lib/bird_grinder/client.rb +136 -0
- data/lib/bird_grinder/command_handler.rb +84 -0
- data/lib/bird_grinder/console.rb +42 -0
- data/lib/bird_grinder/exceptions.rb +6 -0
- data/lib/bird_grinder/loader.rb +5 -0
- data/lib/bird_grinder/queue_processor.rb +86 -0
- data/lib/bird_grinder/tweeter/search.rb +71 -0
- data/lib/bird_grinder/tweeter/stream_processor.rb +46 -0
- data/lib/bird_grinder/tweeter/streaming.rb +74 -0
- data/lib/bird_grinder/tweeter.rb +234 -0
- data/lib/bird_grinder.rb +29 -0
- data/lib/birdgrinder.rb +1 -0
- data/lib/moneta/basic_file.rb +113 -0
- data/lib/moneta/redis.rb +49 -0
- data/templates/boot.erb +3 -0
- data/templates/debug_handler.erb +13 -0
- data/templates/hello_world_handler.erb +10 -0
- data/templates/rakefile.erb +15 -0
- data/templates/settings.yml.erb +4 -0
- data/templates/setup.erb +40 -0
- data/templates/test_helper.erb +17 -0
- data/test/bird_grinder_test.rb +9 -0
- data/test/test_helper.rb +35 -0
- metadata +140 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'readline'
|
2
|
+
require 'irb'
|
3
|
+
require 'irb/completion'
|
4
|
+
|
5
|
+
module BirdGrinder
|
6
|
+
# A simple controller for bringing up an IRB instance with the birdgrinder
|
7
|
+
# environment pre-loaded.
|
8
|
+
class Console
|
9
|
+
|
10
|
+
# Define code here that you want available at the IRB
|
11
|
+
# prompt automatically.
|
12
|
+
module BaseExtensions
|
13
|
+
include BirdGrinder::Loggable
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
setup_irb
|
18
|
+
end
|
19
|
+
|
20
|
+
# Include the base extensions in our top level binding
|
21
|
+
# so they can be accessed at the prompt.
|
22
|
+
def setup_irb
|
23
|
+
# This is a bit hacky, surely there is a better way?
|
24
|
+
# e.g. some way to specify which scope irb runs in.
|
25
|
+
eval("include BirdGrinder::Console::BaseExtensions", TOPLEVEL_BINDING)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Actually starts IRB
|
29
|
+
def run
|
30
|
+
puts "Loading BirdGrinder Console..."
|
31
|
+
# Trick IRB into thinking it has no arguments.
|
32
|
+
ARGV.replace []
|
33
|
+
IRB.start
|
34
|
+
end
|
35
|
+
|
36
|
+
# Starts up a new IRB instance with access to birdgrinder features.
|
37
|
+
def self.run
|
38
|
+
self.new.run
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'em-redis'
|
2
|
+
|
3
|
+
module BirdGrinder
|
4
|
+
# When running, the queue processor makes it possible to
|
5
|
+
# use a redis queue queue up and dispatch tweets and direct
|
6
|
+
# messages from external processes. This is useful since
|
7
|
+
# it makes it easy to have 1 outgoing source of tweets,
|
8
|
+
# triggered from any external application. Included in
|
9
|
+
# examples/bird_grinder_client.rb is a simple example
|
10
|
+
# client which uses redis to queue tweets and dms.
|
11
|
+
class QueueProcessor
|
12
|
+
|
13
|
+
cattr_accessor :polling_delay, :namespace, :action_whitelist
|
14
|
+
# 10 seconds if queue is empty.
|
15
|
+
self.polling_delay = 10
|
16
|
+
self.namespace = 'bg:messages'
|
17
|
+
self.action_whitelist = ["tweet", "dm"]
|
18
|
+
|
19
|
+
is :loggable
|
20
|
+
|
21
|
+
attr_accessor :tweeter
|
22
|
+
|
23
|
+
# Initializes redis and our tweeter.
|
24
|
+
def initialize
|
25
|
+
@tweeter = Tweeter.new(self)
|
26
|
+
@redis = EM::P::Redis.connect
|
27
|
+
end
|
28
|
+
|
29
|
+
# Attempts to pop and process an item from the front of the queue.
|
30
|
+
# Also, it will queue up the next check - if current item was empty,
|
31
|
+
# it will happen after a specified delay otherwise it will check now.
|
32
|
+
def check_queue
|
33
|
+
logger.debug "Checking Redis for outgoing messages"
|
34
|
+
@redis.lpop(@@namespace) do |res|
|
35
|
+
if res.blank?
|
36
|
+
logger.debug "Empty queue, scheduling check in #{@@polling_delay} seconds"
|
37
|
+
schedule_check(@@polling_delay)
|
38
|
+
else
|
39
|
+
logger.debug "Got item, processing and scheduling next check"
|
40
|
+
begin
|
41
|
+
handle_action Yajl::Parser.parse(res)
|
42
|
+
rescue Yajl::ParseError => e
|
43
|
+
logger.error "Couldn't parse json: #{e.message}"
|
44
|
+
end
|
45
|
+
schedule_check
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Check the queue.
|
51
|
+
#
|
52
|
+
# @param [Integer, nil] time the specified delay. If nil, it will be done now.
|
53
|
+
def schedule_check(time = nil)
|
54
|
+
if time == nil
|
55
|
+
check_queue
|
56
|
+
else
|
57
|
+
EventMachine.add_timer(@@polling_delay) { check_queue }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Processes a given action action - calling handle action
|
62
|
+
# if present.
|
63
|
+
def process_action(res)
|
64
|
+
if res.is_a?(Hash) && res["action"].present?
|
65
|
+
handle_action(res["action"], res["arguments"])
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Calls the correct method on the tweeter if present
|
70
|
+
# and in the whitelist. logs and caught argument errors.
|
71
|
+
def handle_action(action, args)
|
72
|
+
args ||= []
|
73
|
+
@tweeter.send(action, *[*args]) if @@action_whitelist.include?(action)
|
74
|
+
rescue ArgumentError
|
75
|
+
logger.warn "Incorrect call for #{action} with arguuments #{args}"
|
76
|
+
end
|
77
|
+
|
78
|
+
# Starts the queue processor with an initial check.
|
79
|
+
# raises an exception if the reactor isn't running.
|
80
|
+
def self.start
|
81
|
+
raise "EventMachine must be running" unless EM.reactor_running?
|
82
|
+
new.check_queue
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module BirdGrinder
|
2
|
+
class Tweeter
|
3
|
+
class Search
|
4
|
+
is :loggable
|
5
|
+
|
6
|
+
# 30 seconds between searches
|
7
|
+
DELAY_SEARCH = 30
|
8
|
+
|
9
|
+
cattr_accessor :search_base_url
|
10
|
+
@@search_base_url = "http://search.twitter.com/"
|
11
|
+
|
12
|
+
def initialize(parent)
|
13
|
+
logger.debug "Initializing Search"
|
14
|
+
@parent = parent
|
15
|
+
end
|
16
|
+
|
17
|
+
# Uses the twitter search api to look up a
|
18
|
+
# given query. If :repeat is given, it will
|
19
|
+
# repeat indefinitely, getting only new messages each
|
20
|
+
# iteration.
|
21
|
+
#
|
22
|
+
# @param [String] query what you wish to search for
|
23
|
+
# @param [Hash] opts options for the query string (except for :repeat)
|
24
|
+
# @option opts [Boolean] :repeat if present, will repeat indefinitely.
|
25
|
+
def search_for(query, opts = {})
|
26
|
+
logger.info "Searching for #{query.inspect}"
|
27
|
+
opts = opts.dup
|
28
|
+
repeat = opts.delete(:repeat)
|
29
|
+
perform_search(query, opts) do |response|
|
30
|
+
if repeat && response.max_id?
|
31
|
+
logger.info "Scheduling next search iteration for #{query.inspect}"
|
32
|
+
EM.add_timer(DELAY_SEARCH) do
|
33
|
+
opts[:repeat] = true
|
34
|
+
opts[:since_id] = response.max_id
|
35
|
+
search_for(query, opts)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
protected
|
42
|
+
|
43
|
+
def perform_search(query, opts = {}, &blk)
|
44
|
+
url = search_base_url / "search.json"
|
45
|
+
query_opts = opts.stringify_keys
|
46
|
+
query_opts["q"] = query.to_s.strip
|
47
|
+
query_opts["rpp"] ||= 100
|
48
|
+
http = EventMachine::HttpRequest.new(url).get(:query => query_opts)
|
49
|
+
http.callback do
|
50
|
+
response = parse_response(http)
|
51
|
+
blk.call(response) if blk.present?
|
52
|
+
if response.results?
|
53
|
+
response.results.each do |result|
|
54
|
+
result.type = :search
|
55
|
+
@parent.delegate.receive_message(:incoming_search, result)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def parse_response(http)
|
62
|
+
response = Yajl::Parser.parse(http.response)
|
63
|
+
response.to_nash.normalized
|
64
|
+
rescue Yajl::ParseError => e
|
65
|
+
logger.error "Couldn't parse search response, error:\n#{e.message}"
|
66
|
+
BirdGrinder::Nash.new
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module BirdGrinder
|
2
|
+
class Tweeter
|
3
|
+
class StreamProcessor
|
4
|
+
is :loggable
|
5
|
+
|
6
|
+
def initialize(parent, stream_name)
|
7
|
+
@parent = parent
|
8
|
+
@stream_name = stream_name.to_sym
|
9
|
+
setup_parser
|
10
|
+
end
|
11
|
+
|
12
|
+
def receive_chunk(chunk)
|
13
|
+
@parser << chunk
|
14
|
+
rescue Yajl::ParseError => e
|
15
|
+
logger.error "Couldn't parse json: #{e.message}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def process_stream_item(json)
|
19
|
+
return if !json.is_a?(Hash)
|
20
|
+
processed = json.to_nash.normalized
|
21
|
+
processed.type = lookup_type_for_steam_response(processed)
|
22
|
+
processed.streaming_source = @stream_name
|
23
|
+
logger.info "Processing Stream Tweet #{processed.id}: #{processed.text}"
|
24
|
+
@parent.delegate.receive_message(:incoming_stream, processed)
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
def lookup_type_for_steam_response(response)
|
30
|
+
if response.delete?
|
31
|
+
:delete
|
32
|
+
elsif response.limit?
|
33
|
+
:limit
|
34
|
+
else
|
35
|
+
:tweet
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def setup_parser
|
40
|
+
@parser = Yajl::Parser.new
|
41
|
+
@parser.on_parse_complete = method(:process_stream_item)
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'bird_grinder/tweeter/stream_processor'
|
2
|
+
|
3
|
+
module BirdGrinder
|
4
|
+
class Tweeter
|
5
|
+
# Basic support for the twitter streaming api. Provides
|
6
|
+
# access to sample, filter, follow and track. Note that
|
7
|
+
# it will dispatch messages as :incoming_stream, with
|
8
|
+
# options.streaming_source set to the stream origin.
|
9
|
+
class Streaming
|
10
|
+
is :loggable
|
11
|
+
|
12
|
+
cattr_accessor :streaming_base_url, :api_version
|
13
|
+
self.streaming_base_url = "http://stream.twitter.com/"
|
14
|
+
self.api_version = 1
|
15
|
+
|
16
|
+
attr_accessor :parent
|
17
|
+
|
18
|
+
def initialize(parent)
|
19
|
+
@parent = parent
|
20
|
+
logger.debug "Initializing Streaming Support"
|
21
|
+
end
|
22
|
+
|
23
|
+
# Start processing the sample stream
|
24
|
+
#
|
25
|
+
# @param [Hash] opts extra options for the query
|
26
|
+
def sample(opts = {})
|
27
|
+
get(:sample, opts)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Start processing the filter stream
|
31
|
+
#
|
32
|
+
# @param [Hash] opts extra options for the query
|
33
|
+
def filter(opts = {})
|
34
|
+
get(:filter, opts)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Start processing the filter stream with a given follow
|
38
|
+
# argument.
|
39
|
+
#
|
40
|
+
# @param [Array] args what to follow, joined with ","
|
41
|
+
def follow(*args)
|
42
|
+
opts = args.extract_options!
|
43
|
+
opts[:follow] = args.join(",")
|
44
|
+
opts[:path] = :filter
|
45
|
+
get(:follow, opts)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Starts tracking a specific query.
|
49
|
+
#
|
50
|
+
# @param [Hash] opts extra options for the query
|
51
|
+
def track(query, opts = {})
|
52
|
+
opts[:track] = query
|
53
|
+
opts[:path] = :filter
|
54
|
+
get(:track, opts)
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
|
59
|
+
def get(name, opts = {}, attempts = 0)
|
60
|
+
logger.debug "Getting stream #{name} w/ options: #{opts.inspect}"
|
61
|
+
path = opts.delete(:path)
|
62
|
+
processor = StreamProcessor.new(@parent, name)
|
63
|
+
http_opts = {
|
64
|
+
:on_response => processor.method(:receive_chunk),
|
65
|
+
:head => {'Authorization' => @parent.auth_credentials}
|
66
|
+
}
|
67
|
+
http_opts[:query] = opts if opts.present?
|
68
|
+
url = streaming_base_url / api_version.to_s / "statuses" / "#{path || name}.json"
|
69
|
+
http = EventMachine::HttpRequest.new(url).get(http_opts)
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,234 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module BirdGrinder
|
4
|
+
# An asynchronous, delegate-based twitter client that uses
|
5
|
+
# em-http-request and yajl on the backend. It's built to be fast,
|
6
|
+
# minimal and easy to use.
|
7
|
+
#
|
8
|
+
# The delegate is simply any class - the tweeter will attempt to
|
9
|
+
# call receive_message([Symbol], [BirdGrinder::Nash]) every time
|
10
|
+
# it processes a message / item of some kind. This in turn makes
|
11
|
+
# it easy to process items. Also, it will dispatch both
|
12
|
+
# incoming (e.g. :incoming_mention, :incoming_direct_message) and
|
13
|
+
# outgoing (e.g. :outgoing_tweet) events.
|
14
|
+
#
|
15
|
+
# It has support the twitter search api (via #search) and the currently-
|
16
|
+
# alpha twitter streaming api (using #streaming) built right in.
|
17
|
+
class Tweeter
|
18
|
+
is :loggable, :delegateable
|
19
|
+
|
20
|
+
require 'bird_grinder/tweeter/streaming'
|
21
|
+
require 'bird_grinder/tweeter/search'
|
22
|
+
|
23
|
+
VALID_FETCHES = [:direct_messages, :mentions]
|
24
|
+
|
25
|
+
cattr_accessor :api_base_url
|
26
|
+
self.api_base_url = "http://twitter.com/"
|
27
|
+
|
28
|
+
attr_reader :auth_credentials
|
29
|
+
|
30
|
+
# Initializes the tweeter with a given delegate. It will use
|
31
|
+
# username and password from your settings file for authorization
|
32
|
+
# with twitter.
|
33
|
+
#
|
34
|
+
# @param [Delegate] delegate the delegate class
|
35
|
+
def initialize(delegate)
|
36
|
+
check_auth!
|
37
|
+
@auth_credentials = [BirdGrinder::Settings.username, BirdGrinder::Settings.password]
|
38
|
+
delegate_to delegate
|
39
|
+
end
|
40
|
+
|
41
|
+
# Automates fetching mentions / direct messages at the same time.
|
42
|
+
#
|
43
|
+
# @param [Array<Symbol>] fetches what to load - :all for all fetches, or names of the fetches otherwise
|
44
|
+
def fetch(*fetches)
|
45
|
+
options = fetches.extract_options!
|
46
|
+
fetches = VALID_FETCHES if fetches == [:all]
|
47
|
+
(fetches & VALID_FETCHES).each do |fetch_type|
|
48
|
+
send(fetch_type, options.dup)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Tells the twitter api to follow a specific user
|
53
|
+
#
|
54
|
+
# @param [String] user the screen_name of the user to follow
|
55
|
+
# @param [Hash] opts extra options to pass in the query string
|
56
|
+
def follow(user, opts = {})
|
57
|
+
user = user.to_s.strip
|
58
|
+
logger.info "Following '#{user}'"
|
59
|
+
post("friendships/create.json", opts.merge(:screen_name => user)) do
|
60
|
+
delegate.receive_message(:outgoing_follow, :user => user)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Tells the twitter api to unfollow a specific user
|
65
|
+
#
|
66
|
+
# @param [String] user the screen_name of the user to unfollow
|
67
|
+
# @param [Hash] opts extra options to pass in the query string
|
68
|
+
def unfollow(user, opts = {})
|
69
|
+
user = user.to_s.strip
|
70
|
+
logger.info "Unfollowing '#{user}'"
|
71
|
+
post("friendships/destroy.json", opts.merge(:screen_name => user)) do
|
72
|
+
delegate.receive_message(:outgoing_unfollow, :user => user)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Updates your current status on twitter with a specific message
|
77
|
+
#
|
78
|
+
# @param [String] message the contents of your tweet
|
79
|
+
# @param [Hash] opts extra options to pass in the query string
|
80
|
+
def tweet(message, opts = {})
|
81
|
+
message = message.to_s.strip
|
82
|
+
logger.debug "Tweeting #{message}"
|
83
|
+
post("statuses/update.json", opts.merge(:status => message)) do |json|
|
84
|
+
delegate.receive_message(:outgoing_tweet, status_to_args(json))
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Sends a direct message to a given user
|
89
|
+
#
|
90
|
+
# @param [String] user the screen_name of the user you wish to dm
|
91
|
+
# @param [String] text the text to send to the user
|
92
|
+
# @param [Hash] opts extra options to pass in the query string
|
93
|
+
def dm(user, text, opts = {})
|
94
|
+
text = text.to_s.strip
|
95
|
+
user = user.to_s.strip
|
96
|
+
logger.debug "DM'ing #{user}: #{text}"
|
97
|
+
post("direct_messages/new.json", opts.merge(:user => user, :text => text)) do
|
98
|
+
delegate.receive_message(:outgoing_direct_message, :user => user, :text => text)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Returns an instance of BirdGrinder::Tweeter::Streaming,
|
103
|
+
# used for accessing the alpha streaming api for twitter.
|
104
|
+
#
|
105
|
+
# @see BirdGrinder::Tweeter::Streaming
|
106
|
+
def streaming
|
107
|
+
@streaming ||= Streaming.new(self)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Uses the twitter search api to look up a given
|
111
|
+
# query, with a set of possible options.
|
112
|
+
#
|
113
|
+
# @param [String] query the query you wish to search for
|
114
|
+
# @param [Hash] opts the opts to query, all except :repeat are sent to twitter.
|
115
|
+
# @option opts [Boolean] :repeat repeat the query indefinitely, fetching new messages each time
|
116
|
+
def search(query, opts = {})
|
117
|
+
@search ||= Search.new(self)
|
118
|
+
@search.search_for(query, opts)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Sends a correctly-formatted at reply to a given user.
|
122
|
+
# If the users screen_name isn't at the start of the tweet,
|
123
|
+
# it will be appended accordingly.
|
124
|
+
#
|
125
|
+
# @param [String] user the user to reply to's screen name
|
126
|
+
# @param [String] test the text to reply with
|
127
|
+
# @param [Hash] opts the options to pass in the query string
|
128
|
+
def reply(user, text, opts = {})
|
129
|
+
user = user.to_s.strip
|
130
|
+
text = text.to_s.strip
|
131
|
+
text = "@#{user} #{text}".strip unless text =~ /^\@#{user}\b/i
|
132
|
+
tweet(text, opts)
|
133
|
+
end
|
134
|
+
|
135
|
+
# Asynchronously fetches the current users (as specified by your settings)
|
136
|
+
# direct messages from the twitter api
|
137
|
+
#
|
138
|
+
# @param [Hash] opts options to pass in the query string
|
139
|
+
def direct_messages(opts = {})
|
140
|
+
logger.debug "Fetching direct messages..."
|
141
|
+
get("direct_messages.json", opts) do |dms|
|
142
|
+
logger.debug "Fetched a total of #{dms.size} direct message(s)"
|
143
|
+
dms.each do |dm|
|
144
|
+
delegate.receive_message(:incoming_direct_message, status_to_args(dm, :direct_message))
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Asynchronously fetches the current users (as specified by your settings)
|
150
|
+
# mentions from the twitter api
|
151
|
+
#
|
152
|
+
# @param [Hash] opts options to pass in the query string
|
153
|
+
def mentions(opts = {})
|
154
|
+
logger.debug "Fetching mentions..."
|
155
|
+
get("statuses/mentions.json", opts) do |mentions|
|
156
|
+
logger.debug "Fetched a total of #{mentions.size} mention(s)"
|
157
|
+
mentions.each do |status|
|
158
|
+
delegate.receive_message(:incoming_mention, status_to_args(status, :mention))
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
protected
|
164
|
+
|
165
|
+
def request(path = "/")
|
166
|
+
EventMachine::HttpRequest.new(api_base_url / path)
|
167
|
+
end
|
168
|
+
|
169
|
+
def get(path, params = {}, &blk)
|
170
|
+
http = request(path).get({
|
171
|
+
:head => {'Authorization' => @auth_credentials},
|
172
|
+
:query => params.stringify_keys
|
173
|
+
})
|
174
|
+
add_response_callback(http, blk)
|
175
|
+
http
|
176
|
+
end
|
177
|
+
|
178
|
+
def post(path, params = {}, &blk)
|
179
|
+
real_params = {}
|
180
|
+
params.each_pair { |k,v| real_params[URI.encode(k.to_s)] = URI.encode(v) }
|
181
|
+
http = request(path).post({
|
182
|
+
:head => {
|
183
|
+
'Authorization' => @auth_credentials,
|
184
|
+
'Content-Type' => 'application/x-www-form-urlencoded'
|
185
|
+
},
|
186
|
+
:body => real_params
|
187
|
+
})
|
188
|
+
add_response_callback(http, blk)
|
189
|
+
http
|
190
|
+
end
|
191
|
+
|
192
|
+
def add_response_callback(http, blk)
|
193
|
+
http.callback do
|
194
|
+
res = parse_response(http)
|
195
|
+
if res.nil?
|
196
|
+
logger.warn "Got back a blank / errored response."
|
197
|
+
elsif successful?(res)
|
198
|
+
blk.call(res) unless blk.blank?
|
199
|
+
else
|
200
|
+
logger.eror "Error: #{res.error} (on #{res.request})"
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def parse_response(http)
|
206
|
+
response = Yajl::Parser.parse(http.response)
|
207
|
+
if response.respond_to?(:to_ary)
|
208
|
+
response.map { |i| i.to_nash }
|
209
|
+
else
|
210
|
+
response.to_nash
|
211
|
+
end
|
212
|
+
rescue Yajl::ParseError => e
|
213
|
+
logger.error "Invalid Response: #{http.response} (#{e.message})"
|
214
|
+
nil
|
215
|
+
end
|
216
|
+
|
217
|
+
def successful?(response)
|
218
|
+
response.respond_to?(:to_nash) ? !response.to_nash.error? : true
|
219
|
+
end
|
220
|
+
|
221
|
+
def status_to_args(status_items, type = :tweet)
|
222
|
+
results = status_items.to_nash.normalized
|
223
|
+
results.type = type
|
224
|
+
results
|
225
|
+
end
|
226
|
+
|
227
|
+
def check_auth!
|
228
|
+
if BirdGrinder::Settings["username"].blank? || BirdGrinder::Settings["username"].blank?
|
229
|
+
raise BirdGrinder::MissingAuthDetails, "Missing twitter username or password."
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
end
|
234
|
+
end
|