bjeanes-twibot 0.1.6

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,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