badboy-jabbot 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,135 @@
1
+ = Jabbot
2
+ Official URL: http://github.com/badboy/jabbot/tree/master
3
+ Jan-Erik Rediger (BadBoy_) (http://badboy.pytalhost.de)
4
+
5
+ == Description
6
+
7
+ Jabbot is a Ruby microframework for creating Jabber/MUC
8
+ bots, heavily inspired by Sinatra and Twibot.
9
+ I modified the code of Twibot to fit my needs.
10
+ The original Twibot code is located at:
11
+ http://github.com/cjohansen/twibot/tree/master
12
+
13
+ A big thank you to Christian Johansen, who wrote the code for Twibot.
14
+ Jabbot is heavily based on his code.
15
+
16
+ == Usage
17
+
18
+ === Simple example
19
+
20
+ require 'jabbot'
21
+
22
+ # Receive messages, and post them publicly
23
+ #
24
+ message do |message, params|
25
+ post message.text
26
+ end
27
+
28
+ # Respond to query if they come from the right crowd
29
+ # post "message" => "user" is just some syntax sugar
30
+ # post "message", "user" will work to
31
+ query :from => [:cjno, :irbno] do |message, params|
32
+ post "#{message.user} I agree" => message.user
33
+ end
34
+
35
+ # Listen in and log tweets
36
+ # (you can use "message :all" too ;)
37
+ message do |message, params|
38
+ MyApp.log_message(message)
39
+ end
40
+
41
+ === Running the bot
42
+
43
+ To run the bot, simply do:
44
+
45
+ ruby bot.rb
46
+
47
+ === Configuration
48
+
49
+ Jabbot looks for a configuration file in ./config/bot.yml. It should contain
50
+ atleast:
51
+
52
+ login: jabber_login
53
+ password: jabber_password
54
+ channel: channel_to_join
55
+ server: server_to_connect_to
56
+
57
+ You can also configure with Ruby:
58
+
59
+ configure do |conf|
60
+ conf.login = "my_account"
61
+ do
62
+
63
+ If you don't specify login and/or password in any of these ways, Jabbot will fail
64
+
65
+ === "Routes"
66
+
67
+ Like Sinatra, and other web app frameworks, Jabbot supports "routes": patterns
68
+ to match incoming tweets and messages:
69
+
70
+ require 'jabbot'
71
+
72
+ message "time :country :city" do |message,params|
73
+ time = MyTimeService.lookup(params[:country], params[:city])
74
+ post "Time is #{time} in #{params[:city]}, #{params[:country]}"
75
+ end
76
+
77
+ You can have several "message" blocks (or "join", "leave", "query" or "subject").
78
+ Every matching block will be called.
79
+
80
+ Jabbot also supports regular expressions as routes:
81
+
82
+ require 'jabbot'
83
+
84
+ message /^time ([^\s]*) ([^\s]*)/ do |message, params|
85
+ # params is an array of matches when using regexp routes
86
+ time = MyTimeService.lookup(params[0], params[1])
87
+ post "Time is #{time} in #{params[:city]}, #{params[:country]}"
88
+ end
89
+
90
+ == Requirements
91
+
92
+ xmpp4r. You'll need atleast 0.4.
93
+ You can get it via rubygems:
94
+ gem install xmpp4r
95
+ or download it from:
96
+ http://home.gna.org/xmpp4r/
97
+
98
+ == Installation
99
+
100
+ gem sources -a http://gems.github.com # you only have to do this once
101
+ gem install badboy-jabbot
102
+
103
+ == Is it Ruby 1.9?
104
+
105
+ All tests passes even on Ruby 1.9.
106
+ Seems like it works :)
107
+
108
+ == Contributors
109
+
110
+ * Christian Johansen (cjohansen) (author of Twibot) - http://www.cjohansen.no
111
+
112
+ == License
113
+
114
+ (The MIT License)
115
+
116
+ Copyright (c) 2009 Jan-Erik Rediger (Badboy_)
117
+
118
+ Permission is hereby granted, free of charge, to any person obtaining
119
+ a copy of this software and associated documentation files (the
120
+ 'Software'), to deal in the Software without restriction, including
121
+ without limitation the rights to use, copy, modify, merge, publish,
122
+ distribute, sublicense, and/or sell copies of the Software, and to
123
+ permit persons to whom the Software is furnished to do so, subject to
124
+ the following conditions:
125
+
126
+ The above copyright notice and this permission notice shall be
127
+ included in all copies or substantial portions of the Software.
128
+
129
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
130
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
131
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
132
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
133
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
134
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
135
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :patch: 0
data/lib/hash.rb ADDED
@@ -0,0 +1,8 @@
1
+ class Hash
2
+ def symbolize_keys!
3
+ replace(inject({}) do |hash,(key,value)|
4
+ hash[key.to_sym] = value.is_a?(Hash) ? value.symbolize_keys! : value
5
+ hash
6
+ end)
7
+ end
8
+ end
data/lib/jabbot.rb ADDED
@@ -0,0 +1,84 @@
1
+ require 'time'
2
+ require 'xmpp4r'
3
+ require 'xmpp4r/muc/helper/simplemucclient'
4
+ #require 'xmpp4r/muc/helper/mucclient'
5
+ require 'xmpp4r/version/helper/simpleresponder'
6
+ require 'yaml'
7
+ require File.join(File.dirname(__FILE__), 'hash')
8
+
9
+ module Jabbot
10
+
11
+ # :stopdoc:
12
+ VERSION = '0.0.1'
13
+ LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
14
+ PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
15
+ # :startdoc:
16
+
17
+ # Returns the version string for the library.
18
+ #
19
+ def self.version
20
+ VERSION
21
+ end
22
+
23
+ # Returns the library path for the module. If any arguments are given,
24
+ # they will be joined to the end of the libray path using
25
+ # <tt>File.join</tt>.
26
+ #
27
+ def self.libpath( *args )
28
+ args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
29
+ end
30
+
31
+ # Returns the lpath for the module. If any arguments are given,
32
+ # they will be joined to the end of the path using
33
+ # <tt>File.join</tt>.
34
+ #
35
+ def self.path( *args )
36
+ args.empty? ? PATH : ::File.join(PATH, args.flatten)
37
+ end
38
+
39
+ # Utility method used to require all files ending in .rb that lie in the
40
+ # directory below this file that has the same name as the filename passed
41
+ # in. Optionally, a specific _directory_ name can be passed in such that
42
+ # the _filename_ does not have to be equivalent to the directory.
43
+ #
44
+ def self.require_all_libs_relative_to( fname, dir = nil )
45
+ dir ||= File.basename(fname, '.*')
46
+ search_me = File.expand_path(File.join(File.dirname(fname), dir, '**', '*.rb'))
47
+ Dir.glob(search_me).sort.each {|rb| require rb }
48
+ end
49
+
50
+ @@app_file = lambda do
51
+ ignore = [
52
+ /lib\/twibot.*\.rb/, # Library
53
+ /\(.*\)/, # Generated code
54
+ /custom_require\.rb/ # RubyGems require
55
+ ]
56
+
57
+ path = caller.map { |line| line.split(/:\d/, 2).first }.find do |file|
58
+ next if ignore.any? { |pattern| file =~ pattern }
59
+ file
60
+ end
61
+
62
+ path || $0
63
+ end.call
64
+
65
+ #
66
+ # File name of the application file. Inspired by Sinatra
67
+ #
68
+ def self.app_file
69
+ @@app_file
70
+ end
71
+
72
+ #
73
+ # Runs application if application file is the script being executed
74
+ #
75
+ def self.run?
76
+ self.app_file == $0
77
+ end
78
+
79
+ end # module Jabbot
80
+
81
+ Thread.abort_on_exception = true
82
+ Jabbot.require_all_libs_relative_to(__FILE__)
83
+
84
+ # EOF
data/lib/jabbot/bot.rb ADDED
@@ -0,0 +1,244 @@
1
+ require 'logger'
2
+ require File.join(File.expand_path(File.dirname(__FILE__)), 'macros')
3
+ require File.join(File.expand_path(File.dirname(__FILE__)), 'handlers')
4
+
5
+ module Jabbot
6
+ #
7
+ # Main bot "controller" class
8
+ #
9
+ class Bot
10
+ include Jabbot::Handlers
11
+ attr_reader :client
12
+ attr_reader :user
13
+
14
+ Message = Struct.new(:user, :text, :time) do
15
+ def to_s
16
+ "#{user}: #{text}"
17
+ end
18
+ end
19
+
20
+ def initialize(options = nil)
21
+ @conf = nil
22
+ @config = options || Jabbot::Config.default << Jabbot::FileConfig.new
23
+ @log = nil
24
+ @abort = false
25
+ @user = []
26
+
27
+ rescue Exception => krash
28
+ raise SystemExit.new(krash.message)
29
+ end
30
+
31
+ #
32
+ # connect to Jabber and join channel
33
+ #
34
+ def connect
35
+ @jid = Jabber::JID.new(login)
36
+ @mucjid = Jabber::JID.new("#{channel}@#{server}")
37
+
38
+ if @jid.node.nil?
39
+ raise "Your Jabber ID must contain a user name and therefore contain one @ character."
40
+ elsif @jid.resource
41
+ raise "If you intend to set a custom resource, put that in the right text field. Remove the slash!"
42
+ elsif @mucjid.node.nil?
43
+ raise "Please set a room name, e.g. myroom@conference.jabber.org"
44
+ elsif @mucjid.resource
45
+ raise "The MUC room must not contain a resource. Remove the slash!"
46
+ else
47
+ @jid.resource = "jabbot1"
48
+ @mucjid.resource = (config[:nick]||="jabbot")
49
+ @user << config[:nick]
50
+ end
51
+
52
+ @client = Jabber::Client.new(@jid)
53
+ @connected = true
54
+ begin
55
+ @client.connect
56
+ @client.auth(password)
57
+ @muc = Jabber::MUC::SimpleMUCClient.new(@client)
58
+ muc_handlers.call(@muc)
59
+ @muc.join(@mucjid)
60
+ rescue => errmsg
61
+ STDERR.write "#{errmsg.class}\n#{errmsg}, #{errmsg.backtrace.join("\n")}"
62
+ exit 1
63
+ end
64
+ end
65
+
66
+ #
67
+ # Run application
68
+ #
69
+ def run!
70
+ puts "Jabbot #{Jabbot::VERSION} imposing as #{login} on #{channel}@#{server}"
71
+
72
+ trap(:INT) do
73
+ puts "\nAnd it's a wrap. See ya soon!"
74
+ exit
75
+ end
76
+
77
+ connect
78
+ poll
79
+ end
80
+
81
+ #
82
+ # just a lame infinite loop to keep the bot alive while he is connected
83
+ # :)
84
+ #
85
+ def poll
86
+ while connected?
87
+ break unless connected?
88
+ sleep 1
89
+ end
90
+ end
91
+
92
+ #
93
+ # still connected?
94
+ #
95
+ def connected?
96
+ @connected
97
+ end
98
+
99
+ #
100
+ # close connection
101
+ #
102
+ def close
103
+ if connected?
104
+ @connected = false
105
+ client.close
106
+ end
107
+ end
108
+ alias_method :quit, :close
109
+
110
+ #
111
+ # send message
112
+ # alternative: send query to user
113
+ #
114
+ def send_message(msg, to=nil)
115
+ @muc.say(msg.to_s, to)
116
+ end
117
+
118
+ #
119
+ # defines what to do on different actions
120
+ #
121
+ def muc_handlers
122
+ Proc.new do |muc|
123
+ muc.on_message do |time, nick, text|
124
+ if time.nil?
125
+ begin
126
+ dispatch_messages(:message, [Message.new(nick, text, Time.now)]) unless nick == config[:nick]
127
+ rescue Exception => boom
128
+ log.fatal boom.inspect
129
+ log.fatal boom.backtrace[0..5].join("\n")
130
+ end
131
+ end
132
+ end
133
+
134
+ muc.on_private_message do |time, nick, text|
135
+ if time.nil?
136
+ begin
137
+ dispatch_messages(:private, [Message.new(nick, text, Time.now)]) unless nick == config[:nick]
138
+ rescue Exception => boom
139
+ log.fatal boom.inspect
140
+ log.fatal boom.backtrace[0..5].join("\n")
141
+ end
142
+ end
143
+ end
144
+
145
+ muc.on_join do |time, nick|
146
+ unless @user.include? nick
147
+ @user << nick
148
+ end
149
+ if time.nil?
150
+ begin
151
+ dispatch_messages(:join, [Message.new(nick, "join", Time.now)]) unless nick == config[:nick]
152
+ rescue Exception => boom
153
+ log.fatal boom.inspect
154
+ log.fatal boom.backtrace[0..5].join("\n")
155
+ end
156
+ end
157
+ end
158
+
159
+ muc.on_leave do |time, nick|
160
+ @user.delete(nick)
161
+ if time.nil?
162
+ begin
163
+ dispatch_messages(:leave, [Message.new(nick, "leave", Time.now)])
164
+ rescue Exception => boom
165
+ log.fatal boom.inspect
166
+ log.fatal boom.backtrace[0..5].join("\n")
167
+ end
168
+ end
169
+ end
170
+
171
+ muc.on_subject do |time, nick, subject|
172
+ if time.nil?
173
+ begin
174
+ dispatch_messages(:subject, [Message.new(nick, subject, Time.now)])
175
+ rescue Exception => boom
176
+ log.fatal boom.inspect
177
+ log.fatal boom.backtrace[0..5].join("\n")
178
+ end
179
+ end
180
+ end
181
+
182
+ # not working
183
+ #muc.on_self_leave do |*args|
184
+ # p args
185
+ #end
186
+ end
187
+ end
188
+
189
+ #
190
+ # Dispatch a collection of messages
191
+ #
192
+ def dispatch_messages(type, messages)
193
+ messages.each { |message| dispatch(type, message) }
194
+ messages.length
195
+ end
196
+
197
+ #
198
+ # Return logger instance
199
+ #
200
+ def log
201
+ return @log if @log
202
+ os = config[:log_file] ? File.open(config[:log_file], "a") : $stdout
203
+ @log = Logger.new(os)
204
+ @log.level = Logger.const_get(config[:log_level] ? config[:log_level].upcase : "INFO")
205
+ @log
206
+ end
207
+
208
+ #
209
+ # Configure bot
210
+ #
211
+ def configure
212
+ yield @config
213
+ @conf = nil
214
+ end
215
+
216
+ private
217
+ #
218
+ # Map configuration settings
219
+ #
220
+ def method_missing(name, *args, &block)
221
+ return super unless config.key?(name)
222
+
223
+ self.class.send(:define_method, name) { config[name] }
224
+ config[name]
225
+ end
226
+
227
+ #
228
+ # Return configuration
229
+ #
230
+ def config
231
+ return @conf if @conf
232
+ @conf = @config.to_hash
233
+ end
234
+ end
235
+ end
236
+
237
+ # Expose DSL
238
+ include Jabbot::Macros
239
+
240
+ # Run bot if macros has been used
241
+ at_exit do
242
+ raise $! if $!
243
+ @@bot.run! if run?
244
+ end
@@ -0,0 +1,102 @@
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
+ :daemonize => false # not yet used!
28
+ }
29
+
30
+ def initialize(settings = {})
31
+ @configs = []
32
+ @settings = settings
33
+ end
34
+
35
+ #
36
+ # Add a configuration object to override given settings
37
+ #
38
+ def add(config)
39
+ @configs << config
40
+ self
41
+ end
42
+
43
+ alias_method :<<, :add
44
+
45
+ #
46
+ # Makes it possible to access configuration settings as attributes
47
+ #
48
+ def method_missing(name, *args, &block)
49
+ regex = /=$/
50
+ attr_name = name.to_s.sub(regex, '').to_sym
51
+ return super if name == attr_name && !@settings.key?(attr_name)
52
+
53
+ if name != attr_name
54
+ @settings[attr_name] = args.first
55
+ end
56
+
57
+ @settings[attr_name]
58
+ end
59
+
60
+ #
61
+ # Merges configurations and returns a hash with all options
62
+ #
63
+ def to_hash
64
+ hash = {}.merge(@settings)
65
+ @configs.each { |conf| hash.merge!(conf.to_hash) }
66
+ hash
67
+ end
68
+
69
+ def self.default
70
+ Config.new({}.merge(DEFAULT))
71
+ end
72
+ end
73
+
74
+ #
75
+ # Configuration from files
76
+ #
77
+ class FileConfig < Config
78
+
79
+ #
80
+ # Accepts a stream or a file to read configuration from
81
+ # Default is to read configuration from ./config/bot.yml
82
+ #
83
+ # If a stream is passed it is not closed from within the method
84
+ #
85
+ def initialize(fos = File.expand_path("config/bot.yml"))
86
+ stream = fos.is_a?(String) ? File.open(fos, "r") : fos
87
+
88
+ begin
89
+ config = YAML.load(stream.read)
90
+ config.symbolize_keys! if config
91
+ rescue Exception => err
92
+ puts err.message
93
+ puts "Unable to load configuration, aborting"
94
+ exit
95
+ ensure
96
+ stream.close if fos.is_a?(String)
97
+ end
98
+
99
+ super config.is_a?(Hash) ? config : {}
100
+ end
101
+ end
102
+ 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
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
@@ -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
@@ -0,0 +1,191 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper')) unless defined?(Jabbot)
2
+
3
+ class TestHandler < Test::Unit::TestCase
4
+ context "pattern writer" do
5
+ should "abort on empty values" do
6
+ handler = Jabbot::Handler.new
7
+
8
+ handler.pattern = nil
9
+ assert_nil handler.instance_eval { @options[:pattern] }
10
+ assert_nil handler.instance_eval { @options[:tokens] }
11
+
12
+ handler.pattern = ""
13
+ assert_nil handler.instance_eval { @options[:pattern] }
14
+ assert_nil handler.instance_eval { @options[:tokens] }
15
+ end
16
+
17
+ should "turn regular pattern into regex" do
18
+ handler = Jabbot::Handler.new
19
+ handler.pattern = "command"
20
+
21
+ assert_equal(/command(\s.+)?/, handler.instance_eval { @options[:pattern] })
22
+ assert_equal 0, handler.instance_eval { @options[:tokens] }.length
23
+ end
24
+
25
+ should "convert single named switch to regex" do
26
+ handler = Jabbot::Handler.new
27
+ handler.pattern = ":command"
28
+
29
+ assert_equal(/([^\s]+)(\s.+)?/, handler.instance_eval { @options[:pattern] })
30
+ assert_equal 1, handler.instance_eval { @options[:tokens] }.length
31
+ assert_equal :command, handler.instance_eval { @options[:tokens].first }
32
+ end
33
+
34
+ should "convert several named switches to regexen" do
35
+ handler = Jabbot::Handler.new
36
+ handler.pattern = ":command fixed_word :subcommand"
37
+
38
+ assert_equal(/([^\s]+) fixed_word ([^\s]+)(\s.+)?/, handler.instance_eval { @options[:pattern] })
39
+ assert_equal 2, handler.instance_eval { @options[:tokens] }.length
40
+ assert_equal :command, handler.instance_eval { @options[:tokens].first }
41
+ assert_equal :subcommand, handler.instance_eval { @options[:tokens][1] }
42
+ end
43
+
44
+ should "convert several named switches to regexen specified by options" do
45
+ handler = Jabbot::Handler.new(":time :hour", :hour => /\d\d/)
46
+
47
+ assert_equal(/([^\s]+) ((?-mix:\d\d))(\s.+)?/, handler.instance_eval { @options[:pattern] })
48
+ assert_equal 2, handler.instance_eval { @options[:tokens] }.length
49
+ assert_equal :time, handler.instance_eval { @options[:tokens].first }
50
+ assert_equal :hour, handler.instance_eval { @options[:tokens][1] }
51
+ end
52
+ end
53
+
54
+ should "recognize empty pattern" do
55
+ handler = Jabbot::Handler.new
56
+ message = message "cjno", "A jabber message"
57
+
58
+ assert handler.recognize?(message)
59
+ end
60
+
61
+ should "recognize empty pattern and allowed user" do
62
+ handler = Jabbot::Handler.new "", :from => "cjno"
63
+ message = message "cjno", "A jabber message"
64
+ assert handler.recognize?(message)
65
+
66
+ handler = Jabbot::Handler.new "", :from => ["cjno", "irbno"]
67
+ assert handler.recognize?(message)
68
+ end
69
+
70
+ should "not recognize empty pattern and disallowed user" do
71
+ handler = Jabbot::Handler.new "", :from => "irbno"
72
+ message = message "cjno", "A jabber message"
73
+ assert !handler.recognize?(message)
74
+
75
+ handler = Jabbot::Handler.new "", :from => ["irbno", "satan"]
76
+ assert !handler.recognize?(message)
77
+ end
78
+
79
+ should "recognize fixed pattern and no user" do
80
+ handler = Jabbot::Handler.new "time"
81
+ message = message "cjno", "time oslo norway"
82
+ assert handler.recognize?(message)
83
+ end
84
+
85
+ should "recognize dynamic pattern and no user" do
86
+ handler = Jabbot::Handler.new "time :city :country"
87
+ message = message "cjno", "time oslo norway"
88
+ assert handler.recognize?(message)
89
+ end
90
+
91
+ should "not recognize dynamic pattern and no user" do
92
+ handler = Jabbot::Handler.new "time :city :country"
93
+ message = message "cjno", "oslo norway what is the time?"
94
+ assert !handler.recognize?(message)
95
+ end
96
+
97
+ should "recognize fixed pattern and user" do
98
+ handler = Jabbot::Handler.new "time", :from => ["cjno", "irbno"]
99
+ message = message "cjno", "time oslo norway"
100
+ assert handler.recognize?(message)
101
+ end
102
+
103
+ should "recognize dynamic pattern and user" do
104
+ handler = Jabbot::Handler.new "time :city :country", :from => ["cjno", "irbno"]
105
+ message = message "cjno", "time oslo norway"
106
+ assert handler.recognize?(message)
107
+ end
108
+
109
+ should "not recognize dynamic pattern and user" do
110
+ handler = Jabbot::Handler.new "time :city :country", :from => ["cjno", "irbno"]
111
+ message = message "dude", "time oslo norway"
112
+ assert !handler.recognize?(message)
113
+ end
114
+
115
+ should "recognize symbol users" do
116
+ handler = Jabbot::Handler.new "time :city :country", :from => [:cjno, :irbno]
117
+ message = message "dude", "time oslo norway"
118
+ assert !handler.recognize?(message)
119
+
120
+ message = message("cjno", "time oslo norway")
121
+ assert handler.recognize?(message)
122
+ end
123
+
124
+ should "recognize messages from allowed users" do
125
+ handler = Jabbot::Handler.new :from => [:cjno, :irbno]
126
+ message = message "cjno", "time oslo norway"
127
+ assert handler.recognize?(message)
128
+ end
129
+
130
+ should "not recognize messages from unallowed users with capital screen names" do
131
+ handler = Jabbot::Handler.new :from => [:cjno, :irbno]
132
+ message = message "Cjno", "time oslo norway"
133
+ assert !handler.recognize?(message)
134
+ end
135
+
136
+ should "accept options as only argument" do
137
+ handler = Jabbot::Handler.new :from => :cjno
138
+ assert_equal(['cjno'], handler.instance_eval { @options[:from] })
139
+ assert_nil handler.instance_eval { @options[:pattern] }
140
+ end
141
+
142
+ should "provide parameters in params hash" do
143
+ handler = Jabbot::Handler.new("time :city :country", :from => ["cjno", "irbno"]) do |message, params|
144
+ assert_equal "oslo", params[:city]
145
+ assert_equal "norway", params[:country]
146
+ end
147
+
148
+ message = message "cjno", "time oslo norway"
149
+ assert handler.recognize?(message)
150
+ handler.dispatch(message)
151
+ end
152
+
153
+ should "call constructor block from handle" do
154
+ handler = Jabbot::Handler.new("time :city :country", :from => ["cjno", "irbno"]) do |message, params|
155
+ raise "Boom!"
156
+ end
157
+
158
+ assert_raise(RuntimeError) do
159
+ handler.handle(nil, nil)
160
+ end
161
+ end
162
+
163
+ should "recognize regular expressions" do
164
+ handler = Jabbot::Handler.new /(?:what|where) is (.*)/i
165
+ message = message "dude", "Where is this shit?"
166
+ assert handler.recognize?(message)
167
+
168
+ message = message "dude", "How is this shit?"
169
+ assert !handler.recognize?(message)
170
+ end
171
+
172
+ should "recognize regular expressions from specific users" do
173
+ handler = Jabbot::Handler.new /(?:what|where) is (.*)/i, :from => "cjno"
174
+ message = message "dude", "Where is this shit?"
175
+ assert !handler.recognize?(message)
176
+
177
+ message = message "cjno", "Where is this shit?"
178
+ assert handler.recognize?(message)
179
+ end
180
+
181
+ should "provide parameters as arrays when matching regular expressions" do
182
+ handler = Jabbot::Handler.new(/time ([^\s]*) ([^\s]*)/) do |message, params|
183
+ assert_equal "oslo", params[0]
184
+ assert_equal "norway", params[1]
185
+ end
186
+
187
+ message = message "cjno", "time oslo norway"
188
+ assert handler.recognize?(message)
189
+ handler.dispatch(message)
190
+ end
191
+ end
data/test/test_hash.rb ADDED
@@ -0,0 +1,34 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper')) unless defined?(Jabbot)
2
+
3
+ class TestHash < Test::Unit::TestCase
4
+ should "convert string keys to symbols" do
5
+ hash = { "one" => 1, "two" => 2 }
6
+ hash.symbolize_keys!
7
+
8
+ assert_equal 1, hash[:one]
9
+ assert_equal 2, hash[:two]
10
+ assert_nil hash["one"]
11
+ assert_nil hash["two"]
12
+ end
13
+
14
+ should "convert string keys and preserve symbol keys" do
15
+ hash = { "one" => 1, :two => 2 }
16
+ hash.symbolize_keys!
17
+
18
+ assert_equal 1, hash[:one]
19
+ assert_equal 2, hash[:two]
20
+ assert_nil hash["one"]
21
+ assert_nil hash["two"]
22
+ end
23
+
24
+ should "convert hashes recursively" do
25
+ hash = { "one" => 1, :two => { "three" => 3, "four" => 4 } }
26
+ hash.symbolize_keys!
27
+
28
+ assert_equal 1, hash[:one]
29
+ assert_equal 3, hash[:two][:three]
30
+ assert_equal 4, hash[:two][:four]
31
+ assert_nil hash["one"]
32
+ assert_nil hash["two"]
33
+ end
34
+ end
@@ -0,0 +1,38 @@
1
+ require 'rubygems'
2
+ if RUBY_VERSION =~ /1\.9/
3
+ Gem.all_load_paths
4
+ gem "thoughtbot-shoulda"
5
+ gem "xmpp4r"
6
+ end
7
+ require 'test/unit'
8
+ require 'shoulda'
9
+ require File.join(File.dirname(__FILE__), '../lib/jabbot')
10
+
11
+ module Test::Unit::Assertions
12
+ def assert_hashes_equal(expected, actual, message = nil)
13
+ full_message = build_message(message, <<EOT, expected.inspect, actual.inspect)
14
+ <?> expected but was
15
+ <?>.
16
+ EOT
17
+ assert_block(full_message) do
18
+ break false if expected.keys.length != actual.keys.length
19
+ expected.keys.all? { |k| expected[k] == actual[k] }
20
+ end
21
+ end
22
+
23
+ def assert_hashes_not_equal(expected, actual, message = nil)
24
+ full_message = build_message(message, <<EOT, expected.inspect, actual.inspect)
25
+ <?> expected but was
26
+ <?>.
27
+ EOT
28
+ assert_block(full_message) do
29
+ break false if expected.keys.length != actual.keys.length
30
+ expected.keys.any? { |k| expected[k] != actual[k] }
31
+ end
32
+ end
33
+ end
34
+
35
+ Message = Struct.new(:user, :text, :time)
36
+ def message(user, text)
37
+ Message.new(user, text, Time.now)
38
+ end
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: badboy-jabbot
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - BadBoy_
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-03-29 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: xmpp4r
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0.4"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: thoughtbot-shoulda
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 2.10.1
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: technicalpickles-jeweler
37
+ type: :development
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 0.10.2
44
+ version:
45
+ description:
46
+ email: badboy@archlinux.us
47
+ executables: []
48
+
49
+ extensions: []
50
+
51
+ extra_rdoc_files:
52
+ - README.rdoc
53
+ files:
54
+ - README.rdoc
55
+ - VERSION.yml
56
+ - lib/jabbot
57
+ - lib/jabbot/config.rb
58
+ - lib/jabbot/handlers.rb
59
+ - lib/jabbot/message.rb
60
+ - lib/jabbot/macros.rb
61
+ - lib/jabbot/bot.rb
62
+ - lib/jabbot.rb
63
+ - lib/hash.rb
64
+ - test/test_helper.rb
65
+ - test/test_bot.rb
66
+ - test/test_hash.rb
67
+ - test/test_config.rb
68
+ - test/test_handler.rb
69
+ has_rdoc: true
70
+ homepage: http://github.com/badboy/jabbot
71
+ post_install_message:
72
+ rdoc_options:
73
+ - --inline-source
74
+ - --charset=UTF-8
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: "0"
82
+ version:
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: "0"
88
+ version:
89
+ requirements: []
90
+
91
+ rubyforge_project:
92
+ rubygems_version: 1.2.0
93
+ signing_key:
94
+ specification_version: 2
95
+ summary: Simple framework for creating Jabber/MUC bots, inspired by Sinatra and Twibot
96
+ test_files: []
97
+