jabbot 0.1.2
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.
- 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
|