jabbot 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +141 -0
- data/Rakefile +45 -0
- data/VERSION.yml +4 -0
- data/lib/hash.rb +8 -0
- data/lib/jabbot.rb +84 -0
- data/lib/jabbot/bot.rb +244 -0
- data/lib/jabbot/config.rb +105 -0
- data/lib/jabbot/handlers.rb +116 -0
- data/lib/jabbot/macros.rb +85 -0
- data/lib/jabbot/message.rb +7 -0
- data/test/test_bot.rb +126 -0
- data/test/test_config.rb +81 -0
- data/test/test_handler.rb +191 -0
- data/test/test_hash.rb +34 -0
- data/test/test_helper.rb +38 -0
- metadata +102 -0
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module Jabbot
|
4
|
+
#
|
5
|
+
# Jabbot configuration. Use either Jabbot::CliConfig.new or
|
6
|
+
# JabbotFileConfig.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 = Jabbot::FileConfig.new
|
10
|
+
# config << Jabbot::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
|
+
:log_level => 'info',
|
24
|
+
:log_file => nil,
|
25
|
+
:login => nil,
|
26
|
+
:password => nil,
|
27
|
+
:nick => 'jabbot',
|
28
|
+
:channel => nil,
|
29
|
+
:server => nil,
|
30
|
+
:resource => nil
|
31
|
+
}
|
32
|
+
|
33
|
+
def initialize(settings = {})
|
34
|
+
@configs = []
|
35
|
+
@settings = settings
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Add a configuration object to override given settings
|
40
|
+
#
|
41
|
+
def add(config)
|
42
|
+
@configs << config
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
alias_method :<<, :add
|
47
|
+
|
48
|
+
#
|
49
|
+
# Makes it possible to access configuration settings as attributes
|
50
|
+
#
|
51
|
+
def method_missing(name, *args, &block)
|
52
|
+
regex = /=$/
|
53
|
+
attr_name = name.to_s.sub(regex, '').to_sym
|
54
|
+
return super if name == attr_name && !@settings.key?(attr_name)
|
55
|
+
|
56
|
+
if name != attr_name
|
57
|
+
@settings[attr_name] = args.first
|
58
|
+
end
|
59
|
+
|
60
|
+
@settings[attr_name]
|
61
|
+
end
|
62
|
+
|
63
|
+
#
|
64
|
+
# Merges configurations and returns a hash with all options
|
65
|
+
#
|
66
|
+
def to_hash
|
67
|
+
hash = {}.merge(@settings)
|
68
|
+
@configs.each { |conf| hash.merge!(conf.to_hash) }
|
69
|
+
hash
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.default
|
73
|
+
Config.new({}.merge(DEFAULT))
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
#
|
78
|
+
# Configuration from files
|
79
|
+
#
|
80
|
+
class FileConfig < Config
|
81
|
+
|
82
|
+
#
|
83
|
+
# Accepts a stream or a file to read configuration from
|
84
|
+
# Default is to read configuration from ./config/bot.yml
|
85
|
+
#
|
86
|
+
# If a stream is passed it is not closed from within the method
|
87
|
+
#
|
88
|
+
def initialize(fos = File.expand_path("config/bot.yml"))
|
89
|
+
stream = fos.is_a?(String) ? File.open(fos, "r") : fos
|
90
|
+
|
91
|
+
begin
|
92
|
+
config = YAML.load(stream.read)
|
93
|
+
config.symbolize_keys! if config
|
94
|
+
rescue Exception => err
|
95
|
+
puts err.message
|
96
|
+
puts "Unable to load configuration, aborting"
|
97
|
+
exit
|
98
|
+
ensure
|
99
|
+
stream.close if fos.is_a?(String)
|
100
|
+
end
|
101
|
+
|
102
|
+
super config.is_a?(Hash) ? config : {}
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module Jabbot
|
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
|
+
:private => [],
|
19
|
+
:join => [],
|
20
|
+
:subject => [],
|
21
|
+
:leave => []
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def handlers=(hash)
|
26
|
+
@handlers = hash
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# A Handler object is an object which can handle a direct message, tweet or
|
32
|
+
# at reply.
|
33
|
+
#
|
34
|
+
class Handler
|
35
|
+
def initialize(pattern = nil, options = {}, &blk)
|
36
|
+
if pattern.is_a?(Hash)
|
37
|
+
options = pattern
|
38
|
+
pattern = nil
|
39
|
+
end
|
40
|
+
|
41
|
+
@options = options
|
42
|
+
@options[:from].collect! { |s| s.to_s } if @options[:from] && @options[:from].is_a?(Array)
|
43
|
+
@options[:from] = [@options[:from].to_s] if @options[:from] && [String, Symbol].include?(@options[:from].class)
|
44
|
+
@handler = nil
|
45
|
+
@handler = block_given? ? blk : nil
|
46
|
+
self.pattern = pattern
|
47
|
+
end
|
48
|
+
|
49
|
+
#
|
50
|
+
# Parse pattern string and set options
|
51
|
+
#
|
52
|
+
def pattern=(pattern)
|
53
|
+
return if pattern.nil? || pattern == ""
|
54
|
+
|
55
|
+
if pattern == :all
|
56
|
+
return
|
57
|
+
end
|
58
|
+
|
59
|
+
if pattern.is_a?(Regexp)
|
60
|
+
@options[:pattern] = pattern
|
61
|
+
return
|
62
|
+
end
|
63
|
+
|
64
|
+
words = pattern.split.collect { |s| s.strip } # Get all words in pattern
|
65
|
+
@options[:tokens] = words.inject([]) do |sum, token| # Find all tokens, ie :symbol :like :names
|
66
|
+
next sum unless token =~ /^:.*/ # Don't process regular words
|
67
|
+
sym = token.sub(":", "").to_sym # Turn token string into symbol, ie ":token" => :token
|
68
|
+
regex = @options[sym] || '[^\s]+' # Fetch regex if configured, else use any character but space matching
|
69
|
+
pattern.sub!(/(^|\s)#{token}(\s|$)/, '\1(' + regex.to_s + ')\2') # Make sure regex captures named switch
|
70
|
+
sum << sym
|
71
|
+
end
|
72
|
+
|
73
|
+
@options[:pattern] = /#{pattern}(\s.+)?/
|
74
|
+
end
|
75
|
+
|
76
|
+
#
|
77
|
+
# Determines if this handler is suited to handle an incoming message
|
78
|
+
#
|
79
|
+
def recognize?(message)
|
80
|
+
return false if @options[:pattern] && message.text !~ @options[:pattern] # Pattern check
|
81
|
+
|
82
|
+
users = @options[:from] ? @options[:from] : nil
|
83
|
+
return false if users && !users.include?(message.user) # Check allowed senders
|
84
|
+
true
|
85
|
+
end
|
86
|
+
|
87
|
+
#
|
88
|
+
# Process message to build params hash and pass message along with params of
|
89
|
+
# to +handle+
|
90
|
+
#
|
91
|
+
def dispatch(message)
|
92
|
+
return unless recognize?(message)
|
93
|
+
@params = {}
|
94
|
+
|
95
|
+
if @options[:pattern] && @options[:tokens]
|
96
|
+
matches = message.text.match(@options[:pattern])
|
97
|
+
@options[:tokens].each_with_index { |token, i| @params[token] = matches[i+1] }
|
98
|
+
@params[:text] = (matches[@options[:tokens].length+1] || "").strip
|
99
|
+
elsif @options[:pattern] && !@options[:tokens]
|
100
|
+
@params = message.text.match(@options[:pattern]).to_a[1..-1] || []
|
101
|
+
else
|
102
|
+
@params[:text] = message.text
|
103
|
+
end
|
104
|
+
|
105
|
+
return handle(message, @params)
|
106
|
+
end
|
107
|
+
|
108
|
+
#
|
109
|
+
# Handle a message. Calls the internal Proc with the message and the params
|
110
|
+
# hash as parameters.
|
111
|
+
#
|
112
|
+
def handle(message, params)
|
113
|
+
@handler.call(message, params) if @handler
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Jabbot
|
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 query(pattern = nil, options = {}, &blk)
|
22
|
+
add_handler(:private, pattern, options, &blk)
|
23
|
+
end
|
24
|
+
alias_method :private_message, :query
|
25
|
+
|
26
|
+
def join(options = {}, &blk)
|
27
|
+
add_handler(:join, /\Ajoin\Z/, options, &blk)
|
28
|
+
end
|
29
|
+
|
30
|
+
def leave(options = {}, &blk)
|
31
|
+
add_handler(:leave, /\Aleave\Z/, options, &blk)
|
32
|
+
end
|
33
|
+
|
34
|
+
def subject(pattern = nil, options = {}, &blk)
|
35
|
+
add_handler(:subject, pattern, options, &blk)
|
36
|
+
end
|
37
|
+
alias_method :topic, :subject
|
38
|
+
|
39
|
+
def client
|
40
|
+
bot.client
|
41
|
+
end
|
42
|
+
|
43
|
+
def close
|
44
|
+
bot.close
|
45
|
+
end
|
46
|
+
alias_method :quit, :close
|
47
|
+
|
48
|
+
def user
|
49
|
+
bot.user
|
50
|
+
end
|
51
|
+
|
52
|
+
def post(msg, to=nil)
|
53
|
+
if msg.is_a?(Hash) && msg.keys.size == 1
|
54
|
+
to = msg.values.first
|
55
|
+
msg = msg.keys.first
|
56
|
+
end
|
57
|
+
bot.send_message(msg, to)
|
58
|
+
end
|
59
|
+
|
60
|
+
def run?
|
61
|
+
!@@bot.nil?
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
def add_handler(type, pattern, options, &blk)
|
66
|
+
bot.add_handler(type, Jabbot::Handler.new(pattern, options, &blk))
|
67
|
+
end
|
68
|
+
|
69
|
+
def bot
|
70
|
+
return @@bot unless @@bot.nil?
|
71
|
+
|
72
|
+
begin
|
73
|
+
@@bot = Jabbot::Bot.new nil
|
74
|
+
rescue Exception
|
75
|
+
@@bot = Jabbot::Bot.new(Jabbot::Config.default)
|
76
|
+
end
|
77
|
+
|
78
|
+
@@bot
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.bot=(bot)
|
82
|
+
@@bot = bot
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/test/test_bot.rb
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper')) unless defined?(Jabbot)
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
class TestBot < Test::Unit::TestCase
|
5
|
+
should "not raise errors when initialized" do
|
6
|
+
assert_nothing_raised do
|
7
|
+
Jabbot::Bot.new Jabbot::Config.new
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
should "raise errors when initialized without config file" do
|
12
|
+
assert_raise SystemExit do
|
13
|
+
Jabbot::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
|
+
Jabbot::Bot.new
|
28
|
+
end
|
29
|
+
|
30
|
+
FileUtils.rm_rf("config")
|
31
|
+
end
|
32
|
+
|
33
|
+
should "provide configuration settings as methods" do
|
34
|
+
bot = Jabbot::Bot.new Jabbot::Config.new(:login => "jabbot")
|
35
|
+
assert_equal "jabbot", bot.login
|
36
|
+
end
|
37
|
+
|
38
|
+
should "return logger instance" do
|
39
|
+
bot = Jabbot::Bot.new(Jabbot::Config.default << Jabbot::Config.new)
|
40
|
+
assert bot.log.is_a?(Logger)
|
41
|
+
end
|
42
|
+
|
43
|
+
should "respect configured log level" do
|
44
|
+
bot = Jabbot::Bot.new(Jabbot::Config.new(:log_level => "info"))
|
45
|
+
assert_equal Logger::INFO, bot.log.level
|
46
|
+
|
47
|
+
bot = Jabbot::Bot.new(Jabbot::Config.new(:log_level => "warn"))
|
48
|
+
assert_equal Logger::WARN, bot.log.level
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class TestBotMacros < Test::Unit::TestCase
|
53
|
+
should "provide configure macro" do
|
54
|
+
assert respond_to?(:configure)
|
55
|
+
end
|
56
|
+
|
57
|
+
should "yield configuration" do
|
58
|
+
Jabbot::Macros.bot = Jabbot::Bot.new Jabbot::Config.default
|
59
|
+
|
60
|
+
conf = nil
|
61
|
+
assert_nothing_raised { configure { |c| conf = c } }
|
62
|
+
assert conf.is_a?(Jabbot::Config)
|
63
|
+
end
|
64
|
+
|
65
|
+
should "add handler" do
|
66
|
+
Jabbot::Macros.bot = Jabbot::Bot.new Jabbot::Config.default
|
67
|
+
|
68
|
+
handler = add_handler(:message, ":command", :from => :cjno)
|
69
|
+
assert handler.is_a?(Jabbot::Handler), handler.class
|
70
|
+
end
|
71
|
+
|
72
|
+
should "provide client macro" do
|
73
|
+
assert respond_to?(:client)
|
74
|
+
end
|
75
|
+
|
76
|
+
should "provide user macro" do
|
77
|
+
assert respond_to?(:user)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class TestBotHandlers < Test::Unit::TestCase
|
82
|
+
|
83
|
+
should "include handlers" do
|
84
|
+
bot = Jabbot::Bot.new(Jabbot::Config.new)
|
85
|
+
|
86
|
+
assert_not_nil bot.handlers
|
87
|
+
assert_not_nil bot.handlers[:message]
|
88
|
+
assert_not_nil bot.handlers[:private]
|
89
|
+
assert_not_nil bot.handlers[:join]
|
90
|
+
assert_not_nil bot.handlers[:leave]
|
91
|
+
assert_not_nil bot.handlers[:subject]
|
92
|
+
end
|
93
|
+
|
94
|
+
should "add handler" do
|
95
|
+
bot = Jabbot::Bot.new(Jabbot::Config.new)
|
96
|
+
bot.add_handler :message, Jabbot::Handler.new
|
97
|
+
assert_equal 1, bot.handlers[:message].length
|
98
|
+
|
99
|
+
bot.add_handler :message, Jabbot::Handler.new
|
100
|
+
assert_equal 2, bot.handlers[:message].length
|
101
|
+
|
102
|
+
bot.add_handler :private, Jabbot::Handler.new
|
103
|
+
assert_equal 1, bot.handlers[:private].length
|
104
|
+
|
105
|
+
bot.add_handler :private, Jabbot::Handler.new
|
106
|
+
assert_equal 2, bot.handlers[:private].length
|
107
|
+
|
108
|
+
bot.add_handler :join, Jabbot::Handler.new
|
109
|
+
assert_equal 1, bot.handlers[:join].length
|
110
|
+
|
111
|
+
bot.add_handler :join, Jabbot::Handler.new
|
112
|
+
assert_equal 2, bot.handlers[:join].length
|
113
|
+
|
114
|
+
bot.add_handler :leave, Jabbot::Handler.new
|
115
|
+
assert_equal 1, bot.handlers[:leave].length
|
116
|
+
|
117
|
+
bot.add_handler :leave, Jabbot::Handler.new
|
118
|
+
assert_equal 2, bot.handlers[:leave].length
|
119
|
+
|
120
|
+
bot.add_handler :subject, Jabbot::Handler.new
|
121
|
+
assert_equal 1, bot.handlers[:subject].length
|
122
|
+
|
123
|
+
bot.add_handler :subject, Jabbot::Handler.new
|
124
|
+
assert_equal 2, bot.handlers[:subject].length
|
125
|
+
end
|
126
|
+
end
|
data/test/test_config.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper')) unless defined?(Jabbot)
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
class TestConfig < Test::Unit::TestCase
|
5
|
+
should "default configuration be a hash" do
|
6
|
+
assert_not_nil Jabbot::Config::DEFAULT
|
7
|
+
assert Jabbot::Config::DEFAULT.is_a?(Hash)
|
8
|
+
end
|
9
|
+
|
10
|
+
should "initialize with no options" do
|
11
|
+
assert_hashes_equal({}, Jabbot::Config.new.settings)
|
12
|
+
end
|
13
|
+
|
14
|
+
should "return config from add" do
|
15
|
+
config = Jabbot::Config.new
|
16
|
+
assert_equal config, config.add(Jabbot::Config.new)
|
17
|
+
end
|
18
|
+
|
19
|
+
should "alias add to <<" do
|
20
|
+
config = Jabbot::Config.new
|
21
|
+
assert config.respond_to?(:<<)
|
22
|
+
assert config << Jabbot::Config.new
|
23
|
+
end
|
24
|
+
|
25
|
+
should "mirror method_missing as config getters" do
|
26
|
+
config = Jabbot::Config.default << Jabbot::Config.new
|
27
|
+
assert_equal Jabbot::Config::DEFAULT[:password], config.password
|
28
|
+
assert_equal Jabbot::Config::DEFAULT[:login], config.login
|
29
|
+
end
|
30
|
+
|
31
|
+
should "mirror missing methods as config setters" do
|
32
|
+
config = Jabbot::Config.default << Jabbot::Config.new
|
33
|
+
assert_equal Jabbot::Config::DEFAULT[:login], config.login
|
34
|
+
|
35
|
+
val = "jabbot"
|
36
|
+
config.login = val+'!'
|
37
|
+
assert_not_equal Jabbot::Config::DEFAULT[:login], config.login
|
38
|
+
assert_equal val+'!', config.login
|
39
|
+
end
|
40
|
+
|
41
|
+
should "not override default hash" do
|
42
|
+
config = Jabbot::Config.default
|
43
|
+
hash = Jabbot::Config::DEFAULT
|
44
|
+
|
45
|
+
config.login = "jabbot"
|
46
|
+
config.password = "secret"
|
47
|
+
|
48
|
+
assert_hashes_not_equal Jabbot::Config::DEFAULT, config.to_hash
|
49
|
+
assert_hashes_equal hash, Jabbot::Config::DEFAULT
|
50
|
+
end
|
51
|
+
|
52
|
+
should "return merged configuration from to_hash" do
|
53
|
+
config = Jabbot::Config.new
|
54
|
+
config.login = "jabbot"
|
55
|
+
config.password = "secret"
|
56
|
+
|
57
|
+
config2 = Jabbot::Config.new({})
|
58
|
+
config2.login = "not_jabbot2"
|
59
|
+
config << config2
|
60
|
+
options = config.to_hash
|
61
|
+
|
62
|
+
assert_equal "secret", options[:password]
|
63
|
+
assert_equal "not_jabbot2", options[:login]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class TestFileConfig < Test::Unit::TestCase
|
68
|
+
should "subclass config for file config" do
|
69
|
+
assert Jabbot::FileConfig.new(StringIO.new).is_a?(Jabbot::Config)
|
70
|
+
end
|
71
|
+
|
72
|
+
should "read settings from stream" do
|
73
|
+
config = Jabbot::FileConfig.new(StringIO.new <<-YAML)
|
74
|
+
login: jabbot
|
75
|
+
password: secret
|
76
|
+
YAML
|
77
|
+
|
78
|
+
assert_equal "jabbot", config.login
|
79
|
+
assert_equal "secret", config.password
|
80
|
+
end
|
81
|
+
end
|