bjeanes-twibot 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,111 @@
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
+ if pattern.is_a?(Regexp)
54
+ @options[:pattern] = pattern
55
+ return
56
+ end
57
+
58
+ words = pattern.split.collect { |s| s.strip } # Get all words in pattern
59
+ @options[:tokens] = words.inject([]) do |sum, token| # Find all tokens, ie :symbol :like :names
60
+ next sum unless token =~ /^:.*/ # Don't process regular words
61
+ sym = token.sub(":", "").to_sym # Turn token string into symbol, ie ":token" => :token
62
+ regex = @options[sym] || '[^\s]+' # Fetch regex if configured, else use any character but space matching
63
+ pattern.sub!(/(^|\s)#{token}(\s|$)/, '\1(' + regex.to_s + ')\2') # Make sure regex captures named switch
64
+ sum << sym
65
+ end
66
+
67
+ @options[:pattern] = /#{pattern}(\s.+)?/
68
+ end
69
+
70
+ #
71
+ # Determines if this handler is suited to handle an incoming message
72
+ #
73
+ def recognize?(message)
74
+ return false if @options[:pattern] && message.text !~ @options[:pattern] # Pattern check
75
+
76
+ users = @options[:from] ? @options[:from] : nil
77
+ sender = message.respond_to?(:sender) ? message.sender : message.user
78
+ return false if users && !users.include?(sender.screen_name.downcase) # Check allowed senders
79
+ true
80
+ end
81
+
82
+ #
83
+ # Process message to build params hash and pass message along with params of
84
+ # to +handle+
85
+ #
86
+ def dispatch(message)
87
+ return unless recognize?(message)
88
+ @params = {}
89
+
90
+ if @options[:pattern] && @options[:tokens]
91
+ matches = message.text.match(@options[:pattern])
92
+ @options[:tokens].each_with_index { |token, i| @params[token] = matches[i+1] }
93
+ @params[:text] = (matches[@options[:tokens].length+1] || "").strip
94
+ elsif @options[:pattern] && !@options[:tokens]
95
+ @params = message.text.match(@options[:pattern]).to_a[1..-1] || []
96
+ else
97
+ @params[:text] = message.text
98
+ end
99
+
100
+ handle(message, @params)
101
+ end
102
+
103
+ #
104
+ # Handle a message. Calls the internal Proc with the message and the params
105
+ # hash as parameters.
106
+ #
107
+ def handle(message, params)
108
+ @handler.call(message, params) if @handler
109
+ end
110
+ end
111
+ 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
@@ -0,0 +1,4 @@
1
+ module Twibot
2
+ module Tweets
3
+ end
4
+ end
data/lib/twibot.rb ADDED
@@ -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.6'
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
data/test/test_bot.rb ADDED
@@ -0,0 +1,185 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper')) unless defined?(Twibot)
2
+ require 'fileutils'
3
+
4
+ class TestBot < Test::Unit::TestCase
5
+ should "not raise errors when initialized" do
6
+ assert_nothing_raised do
7
+ Twibot::Bot.new Twibot::Config.new
8
+ end
9
+ end
10
+
11
+ should "raise errors when initialized without config file" do
12
+ assert_raise SystemExit do
13
+ Twibot::Bot.new
14
+ end
15
+ end
16
+
17
+ should "not raise error on initialize when config file exists" do
18
+ if File.exists?("config")
19
+ FileUtils.rm("config/bot.yml")
20
+ else
21
+ FileUtils.mkdir("config")
22
+ end
23
+
24
+ File.open("config/bot.yml", "w") { |f| f.puts "" }
25
+
26
+ assert_nothing_raised do
27
+ Twibot::Bot.new
28
+ end
29
+
30
+ FileUtils.rm_rf("config")
31
+ end
32
+
33
+ should "provide configuration settings as methods" do
34
+ bot = Twibot::Bot.new Twibot::Config.new(:max_interval => 3)
35
+ assert_equal 3, bot.max_interval
36
+ end
37
+
38
+ should "return logger instance" do
39
+ bot = Twibot::Bot.new(Twibot::Config.default << Twibot::Config.new)
40
+ assert bot.log.is_a?(Logger)
41
+ end
42
+
43
+ should "respect configured log level" do
44
+ bot = Twibot::Bot.new(Twibot::Config.new(:log_level => "info"))
45
+ assert_equal Logger::INFO, bot.log.level
46
+
47
+ bot = Twibot::Bot.new(Twibot::Config.new(:log_level => "warn"))
48
+ assert_equal Logger::WARN, bot.log.level
49
+ end
50
+
51
+ should "should return false from receive without handlers" do
52
+ bot = Twibot::Bot.new(Twibot::Config.new)
53
+ assert !bot.receive_messages
54
+ assert !bot.receive_replies
55
+ assert !bot.receive_tweets
56
+ end
57
+
58
+ should "receive message" do
59
+ bot = Twibot::Bot.new(Twibot::Config.new(:log_level => "error"))
60
+ bot.add_handler(:message, Twibot::Handler.new)
61
+ Twitter::Client.any_instance.expects(:messages).with(:received, {}).returns([twitter_message("cjno", "Hei der!")])
62
+
63
+ assert bot.receive_messages
64
+ end
65
+
66
+ should "remember last received message" do
67
+ bot = Twibot::Bot.new(Twibot::Config.new(:log_level => "error"))
68
+ bot.add_handler(:message, Twibot::Handler.new)
69
+ Twitter::Client.any_instance.expects(:messages).with(:received, {}).returns([twitter_message("cjno", "Hei der!")])
70
+ assert_equal 1, bot.receive_messages
71
+
72
+ Twitter::Client.any_instance.expects(:messages).with(:received, { :since_id => 1 }).returns([])
73
+ assert_equal 0, bot.receive_messages
74
+ end
75
+
76
+ should "receive tweet" do
77
+ bot = Twibot::Bot.new(Twibot::Config.new(:log_level => "error"))
78
+ bot.add_handler(:tweet, Twibot::Handler.new)
79
+ Twitter::Client.any_instance.expects(:timeline_for).with(:me, {}).returns([tweet("cjno", "Hei der!")])
80
+
81
+ assert_equal 1, bot.receive_tweets
82
+ end
83
+
84
+ should "receive friend tweets if configured" do
85
+ bot = Twibot::Bot.new(Twibot::Config.new({:log_level => "error", :include_friends => true}))
86
+ bot.add_handler(:tweet, Twibot::Handler.new)
87
+ Twitter::Client.any_instance.expects(:timeline_for).with(:friends, {}).returns([tweet("cjno", "Hei der!")])
88
+
89
+ assert_equal 1, bot.receive_tweets
90
+ end
91
+
92
+ should "remember received tweets" do
93
+ bot = Twibot::Bot.new(Twibot::Config.new(:log_level => "error"))
94
+ bot.add_handler(:tweet, Twibot::Handler.new)
95
+ Twitter::Client.any_instance.expects(:timeline_for).with(:me, {}).returns([tweet("cjno", "Hei der!")])
96
+ assert_equal 1, bot.receive_tweets
97
+
98
+ Twitter::Client.any_instance.expects(:timeline_for).with(:me, { :since_id => 1 }).returns([])
99
+ assert_equal 0, bot.receive_tweets
100
+ end
101
+
102
+ should "receive reply when tweet starts with login" do
103
+ bot = Twibot::Bot.new(Twibot::Config.new(:log_level => "error", :login => "irbno"))
104
+ bot.add_handler(:reply, Twibot::Handler.new)
105
+ Twitter::Client.any_instance.expects(:status).with(:replies, {}).returns([tweet("cjno", "@irbno Hei der!")])
106
+
107
+ assert_equal 1, bot.receive_replies
108
+ end
109
+
110
+ should "remember received replies" do
111
+ bot = Twibot::Bot.new(Twibot::Config.new(:log_level => "error", :login => "irbno"))
112
+ bot.add_handler(:reply, Twibot::Handler.new)
113
+ Twitter::Client.any_instance.expects(:status).with(:replies, {}).returns([tweet("cjno", "@irbno Hei der!")])
114
+ assert_equal 1, bot.receive_replies
115
+
116
+ Twitter::Client.any_instance.expects(:status).with(:replies, { :since_id => 1 }).returns([])
117
+ assert_equal 0, bot.receive_replies
118
+ end
119
+
120
+ should "use public as default timeline method for tweet 'verb'" do
121
+ bot = Twibot::Bot.new(Twibot::Config.default)
122
+ assert_equal :public, bot.instance_eval { @config.to_hash[:timeline_for] }
123
+ end
124
+ end
125
+
126
+ class TestBotMacros < Test::Unit::TestCase
127
+ should "provide configure macro" do
128
+ assert respond_to?(:configure)
129
+ end
130
+
131
+ should "yield configuration" do
132
+ Twibot::Macros.bot = Twibot::Bot.new Twibot::Config.default
133
+ bot.prompt = false
134
+
135
+ conf = nil
136
+ assert_nothing_raised { configure { |c| conf = c } }
137
+ assert conf.is_a?(Twibot::Config)
138
+ end
139
+
140
+ should "add handler" do
141
+ Twibot::Macros.bot = Twibot::Bot.new Twibot::Config.default
142
+ bot.prompt = false
143
+
144
+ handler = add_handler(:message, ":command", :from => :cjno)
145
+ assert handler.is_a?(Twibot::Handler), handler.class
146
+ end
147
+
148
+ should "provide twitter macro" do
149
+ assert respond_to?(:twitter)
150
+ assert respond_to?(:client)
151
+ end
152
+ end
153
+
154
+ class TestBotHandlers < Test::Unit::TestCase
155
+
156
+ should "include handlers" do
157
+ bot = Twibot::Bot.new(Twibot::Config.new)
158
+
159
+ assert_not_nil bot.handlers
160
+ assert_not_nil bot.handlers[:message]
161
+ assert_not_nil bot.handlers[:reply]
162
+ assert_not_nil bot.handlers[:tweet]
163
+ end
164
+
165
+ should "add handler" do
166
+ bot = Twibot::Bot.new(Twibot::Config.new)
167
+ bot.add_handler :message, Twibot::Handler.new
168
+ assert_equal 1, bot.handlers[:message].length
169
+
170
+ bot.add_handler :message, Twibot::Handler.new
171
+ assert_equal 2, bot.handlers[:message].length
172
+
173
+ bot.add_handler :reply, Twibot::Handler.new
174
+ assert_equal 1, bot.handlers[:reply].length
175
+
176
+ bot.add_handler :reply, Twibot::Handler.new
177
+ assert_equal 2, bot.handlers[:reply].length
178
+
179
+ bot.add_handler :tweet, Twibot::Handler.new
180
+ assert_equal 1, bot.handlers[:tweet].length
181
+
182
+ bot.add_handler :tweet, Twibot::Handler.new
183
+ assert_equal 2, bot.handlers[:tweet].length
184
+ end
185
+ end
@@ -0,0 +1,89 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper')) unless defined?(Twibot)
2
+ require 'stringio'
3
+
4
+ class TestConfig < Test::Unit::TestCase
5
+ should "default configuration be a hash" do
6
+ assert_not_nil Twibot::Config::DEFAULT
7
+ assert Twibot::Config::DEFAULT.is_a?(Hash)
8
+ end
9
+
10
+ should "initialize with no options" do
11
+ assert_hashes_equal({}, Twibot::Config.new.settings)
12
+ end
13
+
14
+ should "return config from add" do
15
+ config = Twibot::Config.new
16
+ assert_equal config, config.add(Twibot::Config.new)
17
+ end
18
+
19
+ should "alias add to <<" do
20
+ config = Twibot::Config.new
21
+ assert config.respond_to?(:<<)
22
+ assert config << Twibot::Config.new
23
+ end
24
+
25
+ should "mirror method_missing as config getters" do
26
+ config = Twibot::Config.default << Twibot::Config.new
27
+ assert_equal Twibot::Config::DEFAULT[:min_interval], config.min_interval
28
+ assert_equal Twibot::Config::DEFAULT[:login], config.login
29
+ end
30
+
31
+ should "mirror missing methods as config setters" do
32
+ config = Twibot::Config.default << Twibot::Config.new
33
+ assert_equal Twibot::Config::DEFAULT[:min_interval], config.min_interval
34
+
35
+ val = config.min_interval
36
+ config.min_interval = val + 5
37
+ assert_not_equal Twibot::Config::DEFAULT[:min_interval], config.min_interval
38
+ assert_equal val + 5, config.min_interval
39
+ end
40
+
41
+ should "not override default hash" do
42
+ config = Twibot::Config.default
43
+ hash = Twibot::Config::DEFAULT
44
+
45
+ config.min_interval = 0
46
+ config.max_interval = 0
47
+
48
+ assert_hashes_not_equal Twibot::Config::DEFAULT, config.to_hash
49
+ assert_hashes_equal hash, Twibot::Config::DEFAULT
50
+ end
51
+
52
+ should "return merged configuration from to_hash" do
53
+ config = Twibot::Config.new
54
+ config.min_interval = 10
55
+ config.max_interval = 10
56
+
57
+ config2 = Twibot::Config.new({})
58
+ config2.min_interval = 1
59
+ config << config2
60
+ options = config.to_hash
61
+
62
+ assert_equal 10, options[:max_interval]
63
+ assert_equal 1, options[:min_interval]
64
+ end
65
+ end
66
+
67
+ class TestCliConfig < Test::Unit::TestCase
68
+ should "configure from options" do
69
+ config = Twibot::CliConfig.new %w{--min-interval 10 --max-interval 15}
70
+ assert_equal 10, config.min_interval
71
+ assert_equal 15, config.max_interval
72
+ end
73
+ end
74
+
75
+ class TestFileConfig < Test::Unit::TestCase
76
+ should "subclass config for file config" do
77
+ assert Twibot::FileConfig.new(StringIO.new).is_a?(Twibot::Config)
78
+ end
79
+
80
+ should "read settings from stream" do
81
+ config = Twibot::FileConfig.new(StringIO.new <<-YAML)
82
+ min_interval: 10
83
+ max_interval: 20
84
+ YAML
85
+
86
+ assert_equal 10, config.min_interval
87
+ assert_equal 20, config.max_interval
88
+ end
89
+ end