mattmueller-twibot 0.1.7.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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