twibot 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/Rakefile +30 -0
- data/Readme.rdoc +158 -0
- data/lib/hash.rb +8 -0
- data/lib/twibot/bot.rb +201 -0
- data/lib/twibot/config.rb +136 -0
- data/lib/twibot/handlers.rb +103 -0
- data/lib/twibot/macros.rb +66 -0
- data/lib/twibot/tweets.rb +4 -0
- data/lib/twibot.rb +87 -0
- data/tasks/ann.rake +80 -0
- data/tasks/bones.rake +20 -0
- data/tasks/gem.rake +201 -0
- data/tasks/git.rake +40 -0
- data/tasks/notes.rake +27 -0
- data/tasks/post_load.rake +34 -0
- data/tasks/rdoc.rake +50 -0
- data/tasks/rubyforge.rake +55 -0
- data/tasks/setup.rb +300 -0
- data/tasks/spec.rake +54 -0
- data/tasks/svn.rake +47 -0
- data/tasks/test.rake +40 -0
- data/test/test_bot.rb +177 -0
- data/test/test_config.rb +89 -0
- data/test/test_handler.rb +148 -0
- data/test/test_hash.rb +34 -0
- data/test/test_helper.rb +43 -0
- data/test/test_twibot.rb +1 -0
- metadata +106 -0
data/History.txt
ADDED
data/Rakefile
ADDED
@@ -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/cjohansen/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 "twitter4r", "0.3.1"
|
data/Readme.rdoc
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
=Twibot
|
2
|
+
Official URL: http://github.com/cjohansen/twibot/tree/master
|
3
|
+
Christian Johansen (http://www.cjohansen.no)
|
4
|
+
|
5
|
+
== DESCRIPTION
|
6
|
+
|
7
|
+
Twibot (pronounced like "Abbot"), is a Ruby microframework for creating Twitter
|
8
|
+
bots, heavily inspired by Sinatra.
|
9
|
+
|
10
|
+
== USAGE
|
11
|
+
|
12
|
+
=== Simple example
|
13
|
+
|
14
|
+
require 'twibot'
|
15
|
+
|
16
|
+
# Receive messages, and tweet them publicly
|
17
|
+
#
|
18
|
+
message do |message, params|
|
19
|
+
post_tweet message
|
20
|
+
end
|
21
|
+
|
22
|
+
# Respond to @replies if they come from the right crowd
|
23
|
+
#
|
24
|
+
reply :from => [:cjno, :irbno] do |message, params|
|
25
|
+
post_tweet "@#{message.sender.screen_name} I agree"
|
26
|
+
end
|
27
|
+
|
28
|
+
# Listen in and log tweets
|
29
|
+
#
|
30
|
+
tweet do |message, params|
|
31
|
+
MyApp.log_tweet(message)
|
32
|
+
end
|
33
|
+
|
34
|
+
=== Running the bot
|
35
|
+
|
36
|
+
To run the bot, simply do:
|
37
|
+
|
38
|
+
ruby bot.rb
|
39
|
+
|
40
|
+
=== Configuration
|
41
|
+
|
42
|
+
Twibot looks for a configuration file in ./config/bot.yml. It should contain
|
43
|
+
atleast:
|
44
|
+
|
45
|
+
login: twitter_login
|
46
|
+
password: twitter_password
|
47
|
+
|
48
|
+
You can also pass configuration as command line arguments:
|
49
|
+
|
50
|
+
ruby bot.rb --login myaccount
|
51
|
+
|
52
|
+
...or configure with Ruby:
|
53
|
+
|
54
|
+
configure do |conf|
|
55
|
+
conf.login = "my_account"
|
56
|
+
do
|
57
|
+
|
58
|
+
If you don't specify login and/or password in any of these ways, Twibot will
|
59
|
+
prompt you for those.
|
60
|
+
|
61
|
+
If you want to change how Twibot is configured, you can setup the bot instance
|
62
|
+
manually and give it only the configuration options you want:
|
63
|
+
|
64
|
+
# Create bot only with default configuration
|
65
|
+
require 'twibot'
|
66
|
+
bot = Twibot::Bot.new(Twibot::Config.default)
|
67
|
+
|
68
|
+
# Application here...
|
69
|
+
|
70
|
+
If you want command line arguments you can do:
|
71
|
+
|
72
|
+
require 'twibot'
|
73
|
+
bot = Twibot::Bot.new(Twibot::Config.default << Twibot::CliConfig.new)
|
74
|
+
|
75
|
+
=== "Routes"
|
76
|
+
|
77
|
+
Like Sinatra, and other web app frameworks, Twibot supports "routes": patterns
|
78
|
+
to match incoming tweets and messages:
|
79
|
+
|
80
|
+
require 'twibot'
|
81
|
+
|
82
|
+
tweet "time :country :city" do |message,params|
|
83
|
+
time = MyTimeService.lookup(params[:country], params[:city])
|
84
|
+
client.message :post, "Time is #{time} in #{params[:city]}, #{params[:country]}"
|
85
|
+
end
|
86
|
+
|
87
|
+
You can have several "tweet" blocks (or "message" or "reply"). The first one to
|
88
|
+
match an incoming tweet/message will handle it.
|
89
|
+
|
90
|
+
=== Working with the Twitter API
|
91
|
+
|
92
|
+
The DSL gives you access to your Twitter client instance through "client" (or "twitter"):
|
93
|
+
|
94
|
+
message do
|
95
|
+
twitter.status :post, "Hello world" # Also: client.status :post, "Hello world"
|
96
|
+
end
|
97
|
+
|
98
|
+
== REQUIREMENTS
|
99
|
+
|
100
|
+
Twitter4r. You'll need atleast 0.3.1, which is currently only available from GitHub.
|
101
|
+
Versions of Twitter4r prior to 0.3.1 does not allow for the since_id parameter to be
|
102
|
+
appended to URLs to the REST API. Twibot needs these to only fetch fresh messages
|
103
|
+
and tweets.
|
104
|
+
|
105
|
+
== INSTALLATION
|
106
|
+
|
107
|
+
gem install twibot
|
108
|
+
|
109
|
+
== Is it Ruby 1.9?
|
110
|
+
|
111
|
+
Unfortunately no. Context, used for Twibots tests is not Ruby 1.9 compliant, which
|
112
|
+
makes it hard to figure out which part is causing trouble. Will be fixed soon. Fork
|
113
|
+
away if you want to help out!
|
114
|
+
|
115
|
+
== Polling
|
116
|
+
|
117
|
+
Twitter pulled the plug on it's xmpp service last year. This means that Twibot backed
|
118
|
+
bots needs to poll the Twitter service to keep up. Twitter has a request limit on 70
|
119
|
+
reqs/hour, so you should configure your bot not to make more than that, else it will
|
120
|
+
fail. You can ask for your bot account to be put on the whitelist which allows you to
|
121
|
+
make 20.000 reqs/hour, and shouldn't be a problem so long as your intentions are good
|
122
|
+
(I think).
|
123
|
+
|
124
|
+
Twibot polls like this:
|
125
|
+
* Poll messages if any message handlers exist
|
126
|
+
* Poll tweets if any tweet or reply handlers exist
|
127
|
+
* Sleep for +interval+ seconds
|
128
|
+
* Go over again
|
129
|
+
|
130
|
+
As long as Twibot finds any messages and/or tweets, the interval stays the same
|
131
|
+
(min_interval configuration switch). If nothing was found however, the interval to
|
132
|
+
sleep is increased by interval_step configuration option. This happens until it
|
133
|
+
reaches max_interval, where it will stay until Twibot finds anything.
|
134
|
+
|
135
|
+
== LICENSE
|
136
|
+
|
137
|
+
(The MIT License)
|
138
|
+
|
139
|
+
Copyright (c) 2009 Christian Johansen
|
140
|
+
|
141
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
142
|
+
a copy of this software and associated documentation files (the
|
143
|
+
'Software'), to deal in the Software without restriction, including
|
144
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
145
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
146
|
+
permit persons to whom the Software is furnished to do so, subject to
|
147
|
+
the following conditions:
|
148
|
+
|
149
|
+
The above copyright notice and this permission notice shall be
|
150
|
+
included in all copies or substantial portions of the Software.
|
151
|
+
|
152
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
153
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
154
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
155
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
156
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
157
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
158
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/lib/hash.rb
ADDED
data/lib/twibot/bot.rb
ADDED
@@ -0,0 +1,201 @@
|
|
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
|
+
@twitter = Twitter::Client.new :login => config[:login], :password => config[:password]
|
19
|
+
@log = nil
|
20
|
+
@abort = false
|
21
|
+
|
22
|
+
@processed = {
|
23
|
+
:message => nil,
|
24
|
+
:reply => nil,
|
25
|
+
:tweet => nil
|
26
|
+
}
|
27
|
+
rescue Exception => krash
|
28
|
+
raise SystemExit.new krash.message
|
29
|
+
end
|
30
|
+
|
31
|
+
def prompt?
|
32
|
+
@prompt
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# Run application
|
37
|
+
#
|
38
|
+
def run!
|
39
|
+
puts "Twibot #{Twibot::VERSION} imposing as @#{login}"
|
40
|
+
|
41
|
+
trap(:INT) do
|
42
|
+
puts "\nAnd it's a wrap. See ya soon!"
|
43
|
+
exit
|
44
|
+
end
|
45
|
+
|
46
|
+
# Make sure we don't process messages and tweets received prior to bot launch
|
47
|
+
messages = @twitter.messages(:received, { :count => 1 })
|
48
|
+
@processed[:message] = messages.first.id if messages.length > 0
|
49
|
+
|
50
|
+
handle_tweets = @handlers[:tweet].length + @handlers[:reply].length > 0
|
51
|
+
tweets = handle_tweets ? @twitter.timeline_for(:me, { :count => 1 }) : []
|
52
|
+
@processed[:tweet] = tweets.first.id if tweets.length > 0
|
53
|
+
@processed[:reply] = tweets.first.id if tweets.length > 0
|
54
|
+
|
55
|
+
poll
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# Poll Twitter API in a loop and pass on messages and tweets when they appear
|
60
|
+
#
|
61
|
+
def poll
|
62
|
+
max = max_interval
|
63
|
+
step = interval_step
|
64
|
+
interval = min_interval
|
65
|
+
|
66
|
+
while !@abort do
|
67
|
+
message_count = 0
|
68
|
+
message_count += receive_messages || 0
|
69
|
+
message_count += receive_replies || 0
|
70
|
+
message_count += receive_tweets || 0
|
71
|
+
|
72
|
+
interval = message_count > 0 ? min_interval : [interval + step, max].min
|
73
|
+
|
74
|
+
log.debug "Sleeping for #{interval}s"
|
75
|
+
sleep interval
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
#
|
80
|
+
# Receive direct messages
|
81
|
+
#
|
82
|
+
def receive_messages
|
83
|
+
type = :message
|
84
|
+
return false unless handlers[type].length > 0
|
85
|
+
|
86
|
+
options = { :since_id => @processed[type] } if @processed[type]
|
87
|
+
dispatch_messages(type, @twitter.messages(:received, options), %w{message messages})
|
88
|
+
end
|
89
|
+
|
90
|
+
#
|
91
|
+
# Receive tweets
|
92
|
+
#
|
93
|
+
def receive_tweets
|
94
|
+
type = :tweet
|
95
|
+
return false unless handlers[type].length > 0
|
96
|
+
|
97
|
+
options = { :id => @processed[type] } if @processed[type]
|
98
|
+
dispatch_messages(type, @twitter.timeline_for(:me, options), %w{tweet tweets})
|
99
|
+
end
|
100
|
+
|
101
|
+
#
|
102
|
+
# Receive tweets that start with @<login>
|
103
|
+
#
|
104
|
+
def receive_replies
|
105
|
+
type = :reply
|
106
|
+
return false unless handlers[type].length > 0
|
107
|
+
|
108
|
+
options = { :id => @processed[type] } if @processed[type]
|
109
|
+
messages = @twitter.timeline_for(:me, options)
|
110
|
+
|
111
|
+
# Pick only messages that start with our name
|
112
|
+
num = dispatch_messages(type, messages.find_all { |t| t.text =~ /^@#{@twitter.send :login}/ }, %w{reply replies})
|
113
|
+
|
114
|
+
# Avoid picking up messages over again
|
115
|
+
@processed[type] = messages.first.id if messages.length > 0
|
116
|
+
num
|
117
|
+
end
|
118
|
+
|
119
|
+
#
|
120
|
+
# Dispatch a collection of messages
|
121
|
+
#
|
122
|
+
def dispatch_messages(type, messages, labels)
|
123
|
+
messages.each { |message| dispatch(type, message) }
|
124
|
+
@processed[type] = messages.first.id if messages.length > 0
|
125
|
+
|
126
|
+
num = messages.length
|
127
|
+
log.info "Received #{num} #{num == 1 ? labels[0] : labels[1]}"
|
128
|
+
num
|
129
|
+
end
|
130
|
+
|
131
|
+
#
|
132
|
+
# Return logger instance
|
133
|
+
#
|
134
|
+
def log
|
135
|
+
return @log if @log
|
136
|
+
os = config[:log_file] ? File.open(config[:log_file], "a") : $stdout
|
137
|
+
@log = Logger.new(os)
|
138
|
+
@log.level = Logger.const_get(config[:log_level] ? config[:log_level].upcase : "INFO")
|
139
|
+
@log
|
140
|
+
end
|
141
|
+
|
142
|
+
#
|
143
|
+
# Configure bot
|
144
|
+
#
|
145
|
+
def configure
|
146
|
+
yield @config
|
147
|
+
@conf = nil
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
#
|
152
|
+
# Map configuration settings
|
153
|
+
#
|
154
|
+
def method_missing(name, *args, &block)
|
155
|
+
return super unless config.key?(name)
|
156
|
+
|
157
|
+
self.class.send(:define_method, name) { config[name] }
|
158
|
+
config[name]
|
159
|
+
end
|
160
|
+
|
161
|
+
#
|
162
|
+
# Return configuration
|
163
|
+
#
|
164
|
+
def config
|
165
|
+
return @conf if @conf
|
166
|
+
@conf = @config.to_hash
|
167
|
+
|
168
|
+
if prompt? && (!@conf[:login] || !@conf[:password])
|
169
|
+
# No need to rescue LoadError - if the gem is missing then config will
|
170
|
+
# be incomplete, something which will be detected elsewhere
|
171
|
+
begin
|
172
|
+
require 'highline'
|
173
|
+
hl = HighLine.new
|
174
|
+
|
175
|
+
@config.login = hl.ask("Twitter login: ") unless @conf[:login]
|
176
|
+
@config.password = hl.ask("Twitter password: ") { |q| q.echo = '*' } unless @conf[:password]
|
177
|
+
@conf = @config.to_hash
|
178
|
+
rescue LoadError
|
179
|
+
raise SystemExit.new <<-HELP
|
180
|
+
Unable to continue without login and password. Do one of the following:
|
181
|
+
1) Install the HighLine gem (gem install highline) to be prompted for credentials
|
182
|
+
2) Create a config/bot.yml with login: and password:
|
183
|
+
3) Put a configure { |conf| conf.login = "..." } block in your bot application
|
184
|
+
4) Run bot with --login and --password options
|
185
|
+
HELP
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
@conf
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# Expose DSL
|
195
|
+
include Twibot::Macros
|
196
|
+
|
197
|
+
# Run bot if macros has been used
|
198
|
+
at_exit do
|
199
|
+
raise $! if $!
|
200
|
+
@@bot.run! if run?
|
201
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module Twibot
|
4
|
+
#
|
5
|
+
# Twibot configuration. Use either Twibot::CliConfig.new or
|
6
|
+
# TwibotFileConfig.new setup a new bot from either command line or file
|
7
|
+
# (respectively). Configurations can be chained so they override each other:
|
8
|
+
#
|
9
|
+
# config = Twibot::FileConfig.new
|
10
|
+
# config << Twibot::CliConfig.new
|
11
|
+
# config.to_hash
|
12
|
+
#
|
13
|
+
# The preceding example will create a configuration which is based on a
|
14
|
+
# configuration file but have certain values overridden from the command line.
|
15
|
+
# This can be used for instance to store everything but the Twitter account
|
16
|
+
# password in your configuration file. Then you can just provide the password
|
17
|
+
# when running the bot.
|
18
|
+
#
|
19
|
+
class Config
|
20
|
+
attr_reader :settings
|
21
|
+
|
22
|
+
DEFAULT = {
|
23
|
+
:min_interval => 30,
|
24
|
+
:max_interval => 300,
|
25
|
+
:interval_step => 10,
|
26
|
+
:log_level => "info",
|
27
|
+
:log_file => nil,
|
28
|
+
:login => nil,
|
29
|
+
:password => nil,
|
30
|
+
:prompt => false,
|
31
|
+
:daemonize => false
|
32
|
+
}
|
33
|
+
|
34
|
+
def initialize(settings = {})
|
35
|
+
@configs = []
|
36
|
+
@settings = settings
|
37
|
+
end
|
38
|
+
|
39
|
+
#
|
40
|
+
# Add a configuration object to override given settings
|
41
|
+
#
|
42
|
+
def add(config)
|
43
|
+
@configs << config
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
alias_method :<<, :add
|
48
|
+
|
49
|
+
#
|
50
|
+
# Makes it possible to access configuration settings as attributes
|
51
|
+
#
|
52
|
+
def method_missing(name, *args, &block)
|
53
|
+
regex = /=$/
|
54
|
+
attr_name = name.to_s.sub(regex, '').to_sym
|
55
|
+
return super if name == attr_name && !@settings.key?(attr_name)
|
56
|
+
|
57
|
+
if name != attr_name
|
58
|
+
@settings[attr_name] = args.first
|
59
|
+
end
|
60
|
+
|
61
|
+
@settings[attr_name]
|
62
|
+
end
|
63
|
+
|
64
|
+
#
|
65
|
+
# Merges configurations and returns a hash with all options
|
66
|
+
#
|
67
|
+
def to_hash
|
68
|
+
hash = {}.merge(@settings)
|
69
|
+
@configs.each { |conf| hash.merge!(conf.to_hash) }
|
70
|
+
hash
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.default
|
74
|
+
Config.new({}.merge(DEFAULT))
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# Configuration from command line
|
80
|
+
#
|
81
|
+
class CliConfig < Config
|
82
|
+
|
83
|
+
def initialize(args = $*)
|
84
|
+
super()
|
85
|
+
|
86
|
+
@parser = OptionParser.new do |opts|
|
87
|
+
opts.banner += "Usage: #{File.basename(Twibot.app_file)} [options]"
|
88
|
+
|
89
|
+
opts.on("-m", "--min-interval SECS", Integer, "Minimum poll interval in seconds") { |i| @settings[:min_interval] = i }
|
90
|
+
opts.on("-x", "--max-interval SECS", Integer, "Maximum poll interval in seconds") { |i| @settings[:max_interval] = i }
|
91
|
+
opts.on("-s", "--interval-step SECS", Integer, "Poll interval step in seconds") { |i| @settings[:interval_step] = i }
|
92
|
+
opts.on("-f", "--log-file FILE", "Log file") { |f| @settings[:log_file] = f }
|
93
|
+
opts.on("-l", "--log-level LEVEL", "Log level (err, warn, info, debug), default id info") { |l| @settings[:log_level] = l }
|
94
|
+
opts.on("-u", "--login LOGIN", "Twitter login") { |l| @settings[:login] = l }
|
95
|
+
opts.on("-p", "--password PASSWORD", "Twitter password") { |p| @settings[:password] = p }
|
96
|
+
opts.on("-h", "--help", "Show this message") { puts opts; exit }
|
97
|
+
|
98
|
+
begin
|
99
|
+
require 'daemons'
|
100
|
+
opts.on("-d", "--daemonize", "Run as background process (Not implemented)") { |t| @settings[:daemonize] = true }
|
101
|
+
rescue LoadError
|
102
|
+
end
|
103
|
+
|
104
|
+
end.parse!(args)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
#
|
109
|
+
# Configuration from files
|
110
|
+
#
|
111
|
+
class FileConfig < Config
|
112
|
+
|
113
|
+
#
|
114
|
+
# Accepts a stream or a file to read configuration from
|
115
|
+
# Default is to read configuration from ./config/bot.yml
|
116
|
+
#
|
117
|
+
# If a stream is passed it is not closed from within the method
|
118
|
+
#
|
119
|
+
def initialize(fos = File.expand_path("config/bot.yml"))
|
120
|
+
stream = fos.is_a?(String) ? File.open(fos, "r") : fos
|
121
|
+
|
122
|
+
begin
|
123
|
+
config = YAML.load(stream.read)
|
124
|
+
config.symbolize_keys! if config
|
125
|
+
rescue Exception => err
|
126
|
+
puts err.message
|
127
|
+
puts "Unable to load configuration, aborting"
|
128
|
+
exit
|
129
|
+
ensure
|
130
|
+
stream.close if fos.is_a?(String)
|
131
|
+
end
|
132
|
+
|
133
|
+
super config.is_a?(Hash) ? config : {}
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Twibot
|
2
|
+
module Handlers
|
3
|
+
#
|
4
|
+
# Add a handler for this bot
|
5
|
+
#
|
6
|
+
def add_handler(type, handler)
|
7
|
+
handlers[type] << handler
|
8
|
+
handler
|
9
|
+
end
|
10
|
+
|
11
|
+
def dispatch(type, message)
|
12
|
+
handlers[type].each { |handler| handler.dispatch(message) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def handlers
|
16
|
+
@handlers ||= {
|
17
|
+
:message => [],
|
18
|
+
:reply => [],
|
19
|
+
:tweet => []
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def handlers=(hash)
|
24
|
+
@handlers = hash
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# A Handler object is an object which can handle a direct message, tweet or
|
30
|
+
# at reply.
|
31
|
+
#
|
32
|
+
class Handler
|
33
|
+
def initialize(pattern = nil, options = {}, &blk)
|
34
|
+
if pattern.is_a?(Hash)
|
35
|
+
options = pattern
|
36
|
+
pattern = nil
|
37
|
+
end
|
38
|
+
|
39
|
+
@options = options
|
40
|
+
@options[:from].collect! { |s| s.to_s } if @options[:from] && @options[:from].is_a?(Array)
|
41
|
+
@options[:from] = [@options[:from].to_s] if @options[:from] && @options[:from].is_a?(String)
|
42
|
+
@handler = nil
|
43
|
+
@handler = block_given? ? blk : nil
|
44
|
+
self.pattern = pattern
|
45
|
+
end
|
46
|
+
|
47
|
+
#
|
48
|
+
# Parse pattern string and set options
|
49
|
+
#
|
50
|
+
def pattern=(pattern)
|
51
|
+
return if pattern.nil? || pattern == ""
|
52
|
+
|
53
|
+
words = pattern.split.collect { |s| s.strip } # Get all words in pattern
|
54
|
+
@options[:tokens] = words.inject([]) do |sum, token| # Find all tokens, ie :symbol :like :names
|
55
|
+
next sum unless token =~ /^:.*/ # Don't process regular words
|
56
|
+
sym = token.sub(":", "").to_sym # Turn token string into symbol, ie ":token" => :token
|
57
|
+
regex = @options[sym] || '[^\s]+' # Fetch regex if configured, else use any character but space matching
|
58
|
+
pattern.sub!(/(^|\s)#{token}(\s|$)/, '\1(' + regex.to_s + ')\2') # Make sure regex captures named switch
|
59
|
+
sum << sym
|
60
|
+
end
|
61
|
+
|
62
|
+
@options[:pattern] = /#{pattern}(\s.+)?/
|
63
|
+
end
|
64
|
+
|
65
|
+
#
|
66
|
+
# Determines if this handler is suited to handle an incoming message
|
67
|
+
#
|
68
|
+
def recognize?(message)
|
69
|
+
return false if @options[:pattern] && message.text !~ @options[:pattern] # Pattern check
|
70
|
+
|
71
|
+
users = @options[:from] ? @options[:from] : nil
|
72
|
+
return false if users && !users.include?(message.sender.screen_name) # Check allowed senders
|
73
|
+
true
|
74
|
+
end
|
75
|
+
|
76
|
+
#
|
77
|
+
# Process message to build params hash and pass message along with params of
|
78
|
+
# to +handle+
|
79
|
+
#
|
80
|
+
def dispatch(message)
|
81
|
+
return unless recognize?(message)
|
82
|
+
@params = {}
|
83
|
+
|
84
|
+
if @options[:pattern]
|
85
|
+
matches = message.text.match(@options[:pattern])
|
86
|
+
@options[:tokens].each_with_index { |token, i| @params[token] = matches[i+1] }
|
87
|
+
@params[:text] = (matches[@options[:tokens].length+1] || "").strip
|
88
|
+
else
|
89
|
+
@params[:text] = message.text
|
90
|
+
end
|
91
|
+
|
92
|
+
handle(message, @params)
|
93
|
+
end
|
94
|
+
|
95
|
+
#
|
96
|
+
# Handle a message. Calls the internal Proc with the message and the params
|
97
|
+
# hash as parameters.
|
98
|
+
#
|
99
|
+
def handle(message, params)
|
100
|
+
@handler.call(message, params) if @handler
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Twibot
|
2
|
+
@@prompt = false
|
3
|
+
|
4
|
+
def self.prompt=(p)
|
5
|
+
@@prompt = f
|
6
|
+
end
|
7
|
+
|
8
|
+
module Macros
|
9
|
+
def self.included(mod)
|
10
|
+
@@bot = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def configure(&blk)
|
14
|
+
bot.configure(&blk)
|
15
|
+
end
|
16
|
+
|
17
|
+
def message(pattern = nil, options = {}, &blk)
|
18
|
+
add_handler(:message, pattern, options, &blk)
|
19
|
+
end
|
20
|
+
|
21
|
+
def reply(pattern = nil, options = {}, &blk)
|
22
|
+
add_handler(:reply, pattern, options, &blk)
|
23
|
+
end
|
24
|
+
|
25
|
+
def tweet(pattern = nil, options = {}, &blk)
|
26
|
+
add_handler(:tweet, pattern, options, &blk)
|
27
|
+
end
|
28
|
+
|
29
|
+
def twitter
|
30
|
+
bot.twitter
|
31
|
+
end
|
32
|
+
|
33
|
+
alias_method :client, :twitter
|
34
|
+
|
35
|
+
def post_tweet(msg)
|
36
|
+
message = msg.respond_to?(:text) ? msg.text : msg
|
37
|
+
puts message
|
38
|
+
client.status(:post, message)
|
39
|
+
end
|
40
|
+
|
41
|
+
def run?
|
42
|
+
!@@bot.nil?
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def add_handler(type, pattern, options, &blk)
|
47
|
+
bot.add_handler(type, Twibot::Handler.new(pattern, options, &blk))
|
48
|
+
end
|
49
|
+
|
50
|
+
def bot
|
51
|
+
return @@bot unless @@bot.nil?
|
52
|
+
|
53
|
+
begin
|
54
|
+
@@bot = Twibot::Bot.new nil, true
|
55
|
+
rescue Exception
|
56
|
+
@@bot = Twibot::Bot.new(Twibot::Config.default << Twibot::CliConfig.new, true)
|
57
|
+
end
|
58
|
+
|
59
|
+
@@bot
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.bot=(bot)
|
63
|
+
@@bot = bot
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|