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.
@@ -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
@@ -0,0 +1,7 @@
1
+ module Jabbot
2
+ Message = Struct.new(:user, :text, :time) do
3
+ def to_s
4
+ "#{user}: #{text}"
5
+ end
6
+ end
7
+ end
@@ -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
@@ -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