mattmueller-twibot 0.1.7.1

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.
@@ -0,0 +1,50 @@
1
+ == 0.1.7 / 2009-06-01
2
+
3
+ * New feature - choose how Twibot processes incoming tweets on startup
4
+ (process all, process new [old behaviour], or process from a given ID)
5
+ Bodaniel Jeanes
6
+ * Substantially improved error handling. Now survives all common network
7
+ stability issues
8
+ * Added a host configuration option. The host name is displayed along all
9
+ output from Twibot. Currently Twitter4R does nothing with this option,
10
+ Twibot knowing about it should make it easier to put Twibot/Twitter4R on
11
+ other services like Laconica instances
12
+
13
+ == 0.1.6 / 2009-04-13
14
+
15
+ * Fixed configure block not actually working for username and password
16
+ Bodaniel Jeanes
17
+ * Minor updates in tests
18
+
19
+ == 0.1.5 / 2009-04-12
20
+
21
+ * Added support for regular expression routes
22
+ * Make timeline_for option configurable, ie in config: timeline_for: :public
23
+ * Fixed bug: Users where unlawfully rejected when their screen name started with
24
+ a capital letter (Wilco)
25
+ * Fixed bug: Twibot crashed if there were no handlers registered
26
+
27
+ == 0.1.4 / 2009-03-24
28
+
29
+ * Removed some warnings
30
+ * Added error handling to avoid Twibot crashing when Twitter is down (Ben Vandgrift)
31
+ * Fixed bug: receiving tweets from named users crashed Twibot (Jens Ohlig)
32
+
33
+ == 0.1.3 / 2009-03-19
34
+
35
+ * Ruby 1.9 support
36
+
37
+ == 0.1.2 / 2009-03-18
38
+
39
+ * Removed some warnings
40
+ * Applied patch from Dan Van Derveer fixing a few minor bugs related to the
41
+ options hash sent to Twitter4R
42
+
43
+ == 0.1.1 / 2009-03-15
44
+
45
+ * Fixed dependency
46
+
47
+ == 0.1.0 / 2009-03-15
48
+
49
+ * 1 major enhancement
50
+ * Birthday!
@@ -0,0 +1,30 @@
1
+ # Look in the tasks/setup.rb file for the various options that can be
2
+ # configured in this Rakefile. The .rake files in the tasks directory
3
+ # are where the options are used.
4
+
5
+ begin
6
+ require 'bones'
7
+ Bones.setup
8
+ rescue LoadError
9
+ begin
10
+ load 'tasks/setup.rb'
11
+ rescue LoadError
12
+ raise RuntimeError, '### please install the "bones" gem ###'
13
+ end
14
+ end
15
+
16
+ ensure_in_path 'lib'
17
+ require 'twibot'
18
+
19
+ task :default => 'test:run'
20
+
21
+ PROJ.name = 'twibot'
22
+ PROJ.authors = 'Christian Johansen'
23
+ PROJ.email = 'christian@cjohansen.no'
24
+ PROJ.url = 'http://github.com/bjeanes/twibot/'
25
+ PROJ.version = Twibot::VERSION
26
+ PROJ.rubyforge.name = 'twibot'
27
+ PROJ.readme_file = 'Readme.rdoc'
28
+ PROJ.rdoc.remote_dir = 'twibot'
29
+
30
+ depend_on "mbbx6spp-twitter4r", "0.3.1"
@@ -0,0 +1,269 @@
1
+ = Twibot
2
+ Official URL: http://github.com/cjohansen/twibot/tree/master
3
+ Christian Johansen (http://www.cjohansen.no)
4
+ Twitter: @cjno
5
+
6
+ == Description
7
+
8
+ Twibot (pronounced like "Abbot"), is a Ruby microframework for creating Twitter
9
+ bots, heavily inspired by Sinatra.
10
+
11
+ == Usage
12
+
13
+ === Simple example
14
+
15
+ require 'twibot'
16
+
17
+ # Receive messages, and tweet them publicly
18
+ #
19
+ message do |message, params|
20
+ post_tweet message
21
+ end
22
+
23
+ # Respond to @replies if they come from the right crowd
24
+ #
25
+ reply :from => [:cjno, :irbno] do |message, params|
26
+ post_reply message, "I agree"
27
+ end
28
+
29
+ # Listen in and log tweets
30
+ #
31
+ tweet do |message, params|
32
+ MyApp.log_tweet(message)
33
+ end
34
+
35
+ # Search for tweets matching a query. The available search operators
36
+ # are explained here: <http://search.twitter.com/operators>
37
+ #
38
+ search "twibot" do |message, params|
39
+ # do_something
40
+ end
41
+
42
+ # Search for tweets with a hashtag
43
+ # see: <http://twitter.pbworks.com/Hashtags>
44
+ #
45
+ # Note: hashtag is just a convenience wrapper
46
+ # around search. It will invoke the search
47
+ # before and after filters.
48
+ #
49
+ hashtag "twibot" do |message, params|
50
+ # do_something
51
+ end
52
+
53
+ # Search for tweets with one of a number of hashtags
54
+ # see: <http://twitter.pbworks.com/Hashtags>
55
+ #
56
+ # Note: hashtags is just an alias to hashtag
57
+ #
58
+ hashtags [:twibot, :ruby, "twitter4r"] do |message, params|
59
+ # do_something
60
+ end
61
+
62
+ # Process any new followers. user_id will be
63
+ # the user's Numeric id and params will always
64
+ # be an empty Hash.
65
+ #
66
+ # add_friend!(id) is a convenience wrapper around the
67
+ # twitter4r friendship method. remove_friend!(id)
68
+ # is also available.
69
+ #
70
+ follower do |user_id, params|
71
+ # keep out the riff-raff...
72
+ bot.add_friend!(user_id) unless user_id == 890631
73
+ end
74
+
75
+ # add some set-up code that will be called
76
+ # before each polling cycle. :all is the
77
+ # default, so it can safely be omitted
78
+ #
79
+ before :all do
80
+ MyApp.log("Started polling at #{Time.now}")
81
+ end
82
+
83
+ # the after hook for the polling cycle gets
84
+ # passed the number of messages that were
85
+ # processed
86
+ #
87
+ after :all do |message_count|
88
+ MyApp.log("Finished polling at #{Time.now}. Got #{message_count} messages.")
89
+ end
90
+
91
+ # each action has before and after hooks available:
92
+ # - follower
93
+ # - message
94
+ # - reply
95
+ # - search
96
+ # - tweet
97
+ #
98
+ # there can be only one before and one after callback
99
+ # registered for a given type. the callback block
100
+ # will be called with no arguments.
101
+ #
102
+ # Note: hashtag and hashtags are just wrappers around
103
+ # search and do not have their own hooks. Use the
104
+ # search hooks when using hashtag or hashtags.
105
+ #
106
+ before :message do
107
+ MyApp.is_processing_a_message = true
108
+ end
109
+
110
+ after :message do
111
+ MyApp.is_processing_a_message = false
112
+ end
113
+
114
+ after :follower do
115
+ MyApp.log("I have another follower!")
116
+ end
117
+
118
+
119
+ === Running the bot
120
+
121
+ To run the bot, simply do:
122
+
123
+ ruby bot.rb
124
+
125
+ === Configuration
126
+
127
+ Twibot looks for a configuration file in ./config/bot.yml. It should contain
128
+ atleast:
129
+
130
+ login: twitter_login
131
+ password: twitter_password
132
+
133
+ You can also pass configuration as command line arguments:
134
+
135
+ ruby bot.rb --login myaccount
136
+
137
+ ...or configure with Ruby:
138
+
139
+ configure do |conf|
140
+ conf.login = "my_account"
141
+ do
142
+
143
+ If you don't specify login and/or password in any of these ways, Twibot will
144
+ prompt you for those.
145
+
146
+ If you want to change how Twibot is configured, you can setup the bot instance
147
+ manually and give it only the configuration options you want:
148
+
149
+ # Create bot only with default configuration
150
+ require 'twibot'
151
+ bot = Twibot::Bot.new(Twibot::Config.default)
152
+
153
+ # Application here...
154
+
155
+ If you want command line arguments you can do:
156
+
157
+ require 'twibot'
158
+ bot = Twibot::Bot.new(Twibot::Config.default << Twibot::CliConfig.new)
159
+
160
+ To disable the buffering of the Twibot log file, set the `log_flush` config
161
+ option to `true`:
162
+
163
+ configure do |conf|
164
+ conf.log_file = File.join(DAEMON_ROOT, 'log', 'twitterd.log')
165
+ conf.log_level = "info"
166
+ conf.log_flush = true
167
+ end
168
+
169
+ === "Routes"
170
+
171
+ Like Sinatra, and other web app frameworks, Twibot supports "routes": patterns
172
+ to match incoming tweets and messages:
173
+
174
+ require 'twibot'
175
+
176
+ tweet "time :country :city" do |message,params|
177
+ time = MyTimeService.lookup(params[:country], params[:city])
178
+ client.message :post, "Time is #{time} in #{params[:city]}, #{params[:country]}"
179
+ end
180
+
181
+ You can have several "tweet" blocks (or "message" or "reply"). The first one to
182
+ match an incoming tweet/message will handle it.
183
+
184
+ As of the upcoming 0.1.5/0.2.0, Twibot also supports regular expressions as routes:
185
+
186
+ require 'twibot'
187
+
188
+ tweet /^time ([^\s]*) ([^\s]*)/ do |message, params|
189
+ # params is an array of matches when using regexp routes
190
+ time = MyTimeService.lookup(params[0], params[1])
191
+ client.message :post, "Time is #{time} in #{params[:city]}, #{params[:country]}"
192
+ end
193
+
194
+ === Working with the Twitter API
195
+
196
+ The DSL gives you access to your Twitter client instance through "client" (or "twitter"):
197
+
198
+ message do
199
+ twitter.status :post, "Hello world" # Also: client.status :post, "Hello world"
200
+ end
201
+
202
+ == Requirements
203
+
204
+ Twitter4r. You'll need atleast 0.3.1, which is currently only available from GitHub.
205
+ Versions of Twitter4r prior to 0.3.1 does not allow for the since_id parameter to be
206
+ appended to URLs to the REST API. Twibot needs these to only fetch fresh messages
207
+ and tweets.
208
+
209
+ == Installation
210
+
211
+ gem install twibot
212
+
213
+ == Is it Ruby 1.9?
214
+
215
+ As of Twibot 0.1.3, yes it is! All tests pass, please give feedback from real world
216
+ usage if you have trouble.
217
+
218
+ == Polling
219
+
220
+ Twitter pulled the plug on it's xmpp service last year. This means that Twibot backed
221
+ bots needs to poll the Twitter service to keep up. Twitter has a request limit on 70
222
+ reqs/hour, so you should configure your bot not to make more than that, else it will
223
+ fail. You can ask for your bot account to be put on the whitelist which allows you to
224
+ make 20.000 reqs/hour, and shouldn't be a problem so long as your intentions are good
225
+ (I think).
226
+
227
+ Twibot polls like this:
228
+ * Poll messages if any message handlers exist
229
+ * Poll tweets if any tweet or reply handlers exist
230
+ * Sleep for +interval+ seconds
231
+ * Go over again
232
+
233
+ As long as Twibot finds any messages and/or tweets, the interval stays the same
234
+ (min_interval configuration switch). If nothing was found however, the interval to
235
+ sleep is increased by interval_step configuration option. This happens until it
236
+ reaches max_interval, where it will stay until Twibot finds anything.
237
+
238
+ == Contributors
239
+
240
+ * Dan Van Derveer (bug fixes) - http://dan.van.derveer.com/
241
+ * Ben Vandgrift (Twitter downtime error handling) - http://neovore.com/
242
+ * Jens Ohlig (warnings)
243
+ * Wilco van Duinkerken (bug fixes) - http://www.sparkboxx.com/
244
+ * Bodaniel Jeanes (configure block fix) - http://bjeanes.github.com/
245
+
246
+ == License
247
+
248
+ (The MIT License)
249
+
250
+ Copyright (c) 2009 Christian Johansen
251
+
252
+ Permission is hereby granted, free of charge, to any person obtaining
253
+ a copy of this software and associated documentation files (the
254
+ 'Software'), to deal in the Software without restriction, including
255
+ without limitation the rights to use, copy, modify, merge, publish,
256
+ distribute, sublicense, and/or sell copies of the Software, and to
257
+ permit persons to whom the Software is furnished to do so, subject to
258
+ the following conditions:
259
+
260
+ The above copyright notice and this permission notice shall be
261
+ included in all copies or substantial portions of the Software.
262
+
263
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
264
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
265
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
266
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
267
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
268
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
269
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,8 @@
1
+ class Hash
2
+ def symbolize_keys!
3
+ replace(inject({}) do |hash,(key,value)|
4
+ hash[key.to_sym] = value.is_a?(Hash) ? value.symbolize_keys! : value
5
+ hash
6
+ end)
7
+ end
8
+ end
@@ -0,0 +1,87 @@
1
+ require 'time'
2
+ require 'twitter'
3
+ require 'twitter/client'
4
+ require 'yaml'
5
+ require File.join(File.dirname(__FILE__), 'hash')
6
+
7
+ module Twibot
8
+
9
+ # :stopdoc:
10
+ VERSION = '0.1.7'
11
+ LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
12
+ PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
13
+ # :startdoc:
14
+
15
+ # Returns the version string for the library.
16
+ #
17
+ def self.version
18
+ VERSION
19
+ end
20
+
21
+ # Returns the library path for the module. If any arguments are given,
22
+ # they will be joined to the end of the libray path using
23
+ # <tt>File.join</tt>.
24
+ #
25
+ def self.libpath( *args )
26
+ args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
27
+ end
28
+
29
+ # Returns the lpath for the module. If any arguments are given,
30
+ # they will be joined to the end of the path using
31
+ # <tt>File.join</tt>.
32
+ #
33
+ def self.path( *args )
34
+ args.empty? ? PATH : ::File.join(PATH, args.flatten)
35
+ end
36
+
37
+ # Utility method used to require all files ending in .rb that lie in the
38
+ # directory below this file that has the same name as the filename passed
39
+ # in. Optionally, a specific _directory_ name can be passed in such that
40
+ # the _filename_ does not have to be equivalent to the directory.
41
+ #
42
+ def self.require_all_libs_relative_to( fname, dir = nil )
43
+ dir ||= File.basename(fname, '.*')
44
+ search_me = File.expand_path(File.join(File.dirname(fname), dir, '**', '*.rb'))
45
+ Dir.glob(search_me).sort.each {|rb| require rb }
46
+ end
47
+
48
+ @@app_file = lambda do
49
+ ignore = [
50
+ /lib\/twibot.*\.rb/, # Library
51
+ /\(.*\)/, # Generated code
52
+ /custom_require\.rb/ # RubyGems require
53
+ ]
54
+
55
+ path = caller.map { |line| line.split(/:\d/, 2).first }.find do |file|
56
+ next if ignore.any? { |pattern| file =~ pattern }
57
+ file
58
+ end
59
+
60
+ path || $0
61
+ end.call
62
+
63
+ #
64
+ # File name of the application file. Inspired by Sinatra
65
+ #
66
+ def self.app_file
67
+ @@app_file
68
+ end
69
+
70
+ #
71
+ # Runs application if application file is the script being executed
72
+ #
73
+ def self.run?
74
+ self.app_file == $0
75
+ end
76
+
77
+ end # module Twibot
78
+
79
+ Twitter::Client.configure do |config|
80
+ config.application_name = 'Twibot'
81
+ config.application_version = Twibot.version
82
+ config.application_url = 'http://github.com/cjohansen/twibot'
83
+ end
84
+
85
+ Twibot.require_all_libs_relative_to(__FILE__)
86
+
87
+ # EOF
@@ -0,0 +1,422 @@
1
+ require 'logger'
2
+ require File.join(File.expand_path(File.dirname(__FILE__)), 'macros')
3
+ require File.join(File.expand_path(File.dirname(__FILE__)), 'handlers')
4
+
5
+ module Twibot
6
+ #
7
+ # Main bot "controller" class
8
+ #
9
+ class Bot
10
+ include Twibot::Handlers
11
+ attr_reader :twitter
12
+ attr_writer :prompt
13
+
14
+ def initialize(options = nil, prompt = false)
15
+ @prompt = prompt
16
+ @conf = nil
17
+ @config = options || Twibot::Config.default << Twibot::FileConfig.new << Twibot::CliConfig.new
18
+ @log = nil
19
+ @abort = false
20
+ rescue Exception => krash
21
+ raise SystemExit.new(krash.message)
22
+ end
23
+
24
+ def prompt?
25
+ @prompt
26
+ end
27
+
28
+ def processed
29
+ @processed ||= {
30
+ :message => nil,
31
+ :reply => nil,
32
+ :tweet => nil,
33
+ :search => {}
34
+ }
35
+ end
36
+
37
+ def twitter
38
+ @twitter ||= Twitter::Client.new(:login => config[:login],
39
+ :password => config[:password],
40
+ :host => config[:host])
41
+ end
42
+
43
+ #
44
+ # Run application
45
+ #
46
+ def run!
47
+ puts "Twibot #{Twibot::VERSION} imposing as @#{login} on #{config[:host]}"
48
+
49
+ trap(:INT) do
50
+ puts "\nAnd it's a wrap. See ya soon!"
51
+ exit
52
+ end
53
+
54
+ case config[:process]
55
+ when :all, nil
56
+ # do nothing so it will fetch ALL
57
+ when :new
58
+ # Make sure we don't process messages and tweets received prior to bot launch
59
+ messages = twitter.messages(:received, { :count => 1 })
60
+ processed[:message] = messages.first.id if messages.length > 0
61
+
62
+ handle_tweets = !handlers.nil? && handlers_for_type(:tweet).length + handlers_for_type(:reply).length + handlers_for_type(:search).keys.length > 0
63
+ # handle_tweets ||= handlers_for_type(:search).keys.length > 0
64
+ tweets = []
65
+
66
+ sandbox do
67
+ tweets = handle_tweets ? twitter.timeline_for(config[:timeline_for], { :count => 1 }) : []
68
+ end
69
+
70
+ processed[:tweet] = tweets.first.id if tweets.length > 0
71
+ processed[:reply] = tweets.first.id if tweets.length > 0
72
+
73
+ # for searches, use latest tweet on public timeline
74
+ #
75
+ if handle_tweets && config[:timeline_for].to_s != "public"
76
+ sandbox { tweets = twitter.timeline_for(:public, { :count => 1 }) }
77
+ end
78
+ if tweets.length > 0
79
+ handlers_for_type(:search).each_key {|q| processed[:search][q] = tweets.first.id }
80
+ end
81
+
82
+ load_followers
83
+
84
+ when Numeric, /\d+/ # a tweet ID to start from
85
+ processed[:tweet] = processed[:reply] = processed[:message] = config[:process]
86
+ handlers[:search].each_key {|q| processed[:search][q] = config[:process] }
87
+ else abort "Unknown process option #{config[:process]}, aborting..."
88
+ end
89
+
90
+ load_friends unless handlers_for_type(:follower).empty?
91
+
92
+ poll
93
+ end
94
+
95
+ #
96
+ # Poll Twitter API in a loop and pass on messages and tweets when they appear
97
+ #
98
+ def poll
99
+ max = max_interval
100
+ step = interval_step
101
+ interval = min_interval
102
+
103
+ while !@abort do
104
+ run_hook :before_all
105
+ message_count = 0
106
+ message_count += receive_messages || 0
107
+ message_count += receive_replies || 0
108
+ message_count += receive_tweets || 0
109
+ message_count += receive_searches || 0
110
+
111
+ receive_followers
112
+
113
+ run_hook :after_all, message_count
114
+
115
+ interval = message_count > 0 ? min_interval : [interval + step, max].min
116
+
117
+ log.debug "#{config[:host]} sleeping for #{interval}s"
118
+ sleep interval
119
+ end
120
+ end
121
+
122
+
123
+ def friend_ids
124
+ @friend_ids ||= {}
125
+ end
126
+
127
+ def add_friend!(user_or_id, only_local=false)
128
+ id = id_for_user_or_id(user_or_id)
129
+ sandbox(0) { twitter.friend(:add, id) } unless only_local
130
+ friend_ids[id] = true
131
+ end
132
+
133
+ def remove_friend!(user_or_id, only_local=false)
134
+ id = id_for_user_or_id(user_or_id)
135
+ sandbox(0) { twitter.friend(:remove, id) } unless only_local
136
+ friend_ids[id] = false
137
+ end
138
+
139
+ def is_friend?(user_or_id)
140
+ !!friend_ids[id_for_user_or_id(user_or_id)]
141
+ end
142
+
143
+ def follower_ids
144
+ @follower_ids ||= {}
145
+ end
146
+
147
+ def add_follower!(user_or_id)
148
+ follower_ids[id_for_user_or_id(user_or_id)] = true
149
+ end
150
+
151
+ def remove_follower!(user_or_id)
152
+ follower_ids[id_for_user_or_id(user_or_id)] = false
153
+ end
154
+
155
+ def is_follower?(user_or_id)
156
+ !!follower_ids[id_for_user_or_id(user_or_id)]
157
+ end
158
+
159
+ def id_for_user_or_id(user_or_id)
160
+ (user_or_id.respond_to?(:screen_name) ? user_or_id.id : user_or_id).to_i
161
+ end
162
+
163
+
164
+ #
165
+ # retrieve a list of friend ids and store it as a Hash
166
+ #
167
+ def load_friends
168
+ sandbox(0) do
169
+ twitter.graph(:friends, config[:login]).each {|id| add_friend!(id, true) }
170
+ end
171
+ end
172
+
173
+ #
174
+ # retrieve a list of friend ids and store it as a Hash
175
+ #
176
+ def load_followers
177
+ sandbox(0) do
178
+ twitter.graph(:followers, config[:login]).each {|id| add_follower!(id) }
179
+ end
180
+ end
181
+
182
+
183
+ #
184
+ # returns a Hash of all registered hooks
185
+ #
186
+ def hooks
187
+ @hooks ||= {}
188
+ end
189
+
190
+ #
191
+ # registers a block to be called at the given +event+
192
+ #
193
+ def add_hook(event, &blk)
194
+ hooks[event.to_sym] = blk
195
+ end
196
+
197
+ #
198
+ # calls the hook method for the +event+ if one has
199
+ # been defined
200
+ #
201
+ def run_hook(event, *args)
202
+ hooks[event.to_sym].call(*args) if hooks[event.to_sym].respond_to? :call
203
+ end
204
+
205
+ #
206
+ # Receive direct messages
207
+ #
208
+ def receive_messages
209
+ type = :message
210
+ return false unless handlers_for_type(type).length > 0
211
+ options = {}
212
+ options[:since_id] = processed[type] if processed[type]
213
+
214
+ sandbox(0) do
215
+ dispatch_messages(type, twitter.messages(:received, options), %w{message messages})
216
+ end
217
+ end
218
+
219
+ #
220
+ # Receive tweets
221
+ #
222
+ def receive_tweets
223
+ type = :tweet
224
+ return false unless handlers_for_type(type).length > 0
225
+ options = {}
226
+ options[:since_id] = processed[type] if processed[type]
227
+
228
+ sandbox(0) do
229
+ dispatch_messages(type, twitter.timeline_for(config.to_hash[:timeline_for] || :public, options), %w{tweet tweets})
230
+ end
231
+ end
232
+
233
+ #
234
+ # Receive tweets that start with @<login>
235
+ #
236
+ def receive_replies
237
+ type = :reply
238
+ return false unless handlers_for_type(type).length > 0
239
+ options = {}
240
+ options[:since_id] = processed[type] if processed[type]
241
+
242
+ sandbox(0) do
243
+ dispatch_messages(type, twitter.status(:replies, options), %w{reply replies})
244
+ end
245
+ end
246
+
247
+ #
248
+ # Receive tweets that match the query parameters
249
+ #
250
+ def receive_searches
251
+ result_count = 0
252
+
253
+ handlers_for_type(:search).each_pair do |query, search_handlers|
254
+ options = { :q => query, :rpp => 100 }
255
+ [:lang, :geocode].each do |param|
256
+ options[param] = search_handlers.first.options[param] if search_handlers.first.options[param]
257
+ end
258
+ options[:since_id] = processed[:search][query] if processed[:search][query]
259
+
260
+ result_count += sandbox(0) do
261
+ dispatch_messages([:search, query], twitter.search(options.merge(options)), %w{tweet tweets}.map {|l| "#{l} for \"#{query}\""})
262
+ end
263
+ end
264
+
265
+ result_count
266
+ end
267
+
268
+ #
269
+ # Receive any new followers
270
+ #
271
+ def receive_followers
272
+ newbies = []
273
+ sandbox(0) do
274
+ twitter.graph(:followers, config[:login]).each {|id| newbies << id unless is_friend?(id) or is_follower?(id) }
275
+ newbies.each do |id|
276
+ add_follower!(id)
277
+ with_hooks(:follower) { handlers_for_type(:follower).each {|h| h.handle(id, {}) } }
278
+ end
279
+ end
280
+ log.info "#{config[:host]}: Received #{newbies.size} new #{newbies.size == 1 ? 'follower' : 'followers'}"
281
+ end
282
+
283
+ #
284
+ # Dispatch a collection of messages
285
+ #
286
+ def dispatch_messages(type, messages, labels)
287
+ messages.each {|message| with_hooks(type) { dispatch(type, message) } }
288
+ # Avoid picking up messages over again
289
+ if type.is_a? Array # [TODO] (mikedemers) this is an ugly hack
290
+ processed[type.first][type.last] = messages.first.id if messages.length > 0
291
+ else
292
+ processed[type] = messages.first.id if messages.length > 0
293
+ end
294
+
295
+ num = messages.length
296
+ log.info "#{config[:host]}: Received #{num} #{num == 1 ? labels[0] : labels[1]}"
297
+ num
298
+ end
299
+
300
+ #
301
+ # invokes the given block, running the before and
302
+ # after hooks for the given type
303
+ #
304
+ def with_hooks(type, &blk)
305
+ event = type.is_a?(Array) ? type.first : type
306
+ run_hook :"before_#{event}"
307
+ value = yield
308
+ run_hook :"after_#{event}"
309
+ value
310
+ end
311
+
312
+ #
313
+ # Return logger instance
314
+ #
315
+ def log
316
+ return @log if @log
317
+ os = config[:log_file] ? File.open(config[:log_file], "a") : $stdout
318
+ os.sync = !!config[:log_flush]
319
+ @log = Logger.new(os)
320
+ @log.level = Logger.const_get(config[:log_level] ? config[:log_level].upcase : "INFO")
321
+ @log
322
+ end
323
+
324
+ #
325
+ # Configure bot
326
+ #
327
+ def configure
328
+ yield @config
329
+ @conf = nil
330
+ @twitter = nil
331
+ end
332
+
333
+ private
334
+ #
335
+ # Map configuration settings
336
+ #
337
+ def method_missing(name, *args, &block)
338
+ return super unless config.key?(name)
339
+
340
+ self.class.send(:define_method, name) { config[name] }
341
+ config[name]
342
+ end
343
+
344
+ #
345
+ # Return configuration
346
+ #
347
+ def config
348
+ return @conf if @conf
349
+ @conf = @config.to_hash
350
+
351
+ if prompt? && (!@conf[:login] || !@conf[:password])
352
+ # No need to rescue LoadError - if the gem is missing then config will
353
+ # be incomplete, something which will be detected elsewhere
354
+ begin
355
+ require 'highline'
356
+ hl = HighLine.new
357
+
358
+ @config.login = hl.ask("Twitter login: ") unless @conf[:login]
359
+ @config.password = hl.ask("Twitter password: ") { |q| q.echo = '*' } unless @conf[:password]
360
+ @conf = @config.to_hash
361
+ rescue LoadError
362
+ raise SystemExit.new( <<-HELP
363
+ Unable to continue without login and password. Do one of the following:
364
+ 1) Install the HighLine gem (gem install highline) to be prompted for credentials
365
+ 2) Create a config/bot.yml with login: and password:
366
+ 3) Put a configure { |conf| conf.login = "..." } block in your bot application
367
+ 4) Run bot with --login and --password options
368
+ HELP
369
+ )
370
+ end
371
+ end
372
+
373
+ @conf
374
+ end
375
+
376
+ #
377
+ # Takes a block and executes it in a sandboxed network environment. It
378
+ # catches and logs most common network connectivity and timeout errors.
379
+ #
380
+ # The method takes an optional parameter. If set, this value will be
381
+ # returned in case an error was raised.
382
+ #
383
+ def sandbox(return_value = nil)
384
+ begin
385
+ return_value = yield
386
+ rescue Twitter::RESTError => e
387
+ log.error("Failed to connect to Twitter. It's likely down for a bit:")
388
+ log.error(e.to_s)
389
+ rescue Errno::ECONNRESET => e
390
+ log.error("Connection was reset")
391
+ log.error(e.to_s)
392
+ rescue Timeout::Error => e
393
+ log.error("Timeout")
394
+ log.error(e.to_s)
395
+ rescue EOFError => e
396
+ log.error(e.to_s)
397
+ rescue Errno::ETIMEDOUT => e
398
+ log.error("Timeout")
399
+ log.error(e.to_s)
400
+ rescue JSON::ParserError => e
401
+ log.error("JSON Parsing error")
402
+ log.error(e.to_s)
403
+ rescue OpenSSL::SSL::SSLError => e
404
+ log.error("SSL error")
405
+ log.error(e.to_s)
406
+ rescue SystemStackError => e
407
+ log.error(e.to_s)
408
+ end
409
+
410
+ return return_value
411
+ end
412
+ end
413
+ end
414
+
415
+ # Expose DSL
416
+ include Twibot::Macros
417
+
418
+ # Run bot if macros has been used
419
+ at_exit do
420
+ raise $! if $!
421
+ @@bot.run! if run?
422
+ end