+module ::Irc
+class Bot
+ # A WebMessage is a web request and response object combined with helper methods.
+ #
+ class WebMessage
+ # Bot instance
+ #
+ attr_reader :bot
+ # HTTP method (POST, GET, etc.)
+ #
+ attr_reader :method
+ # Request object, a instance of WEBrick::HTTPRequest ({http://www.ruby-doc.org/stdlib-2.0/libdoc/webrick/rdoc/WEBrick/HTTPRequest.html docs})
+ #
+ attr_reader :req
+ # Response object, a instance of WEBrick::HTTPResponse ({http://www.ruby-doc.org/stdlib-2.0/libdoc/webrick/rdoc/WEBrick/HTTPResponse.html docs})
+ #
+ attr_reader :res
+ # Parsed post request parameters.
+ #
+ attr_reader :post
+ # Parsed url parameters.
+ #
+ attr_reader :args
+ # Client IP.
+ #
+ attr_reader :client
+ # URL Path.
+ #
+ attr_reader :path
+ # The bot user issuing the command.
+ #
+ attr_reader :source
+ def initialize(bot, req, res)
+ @bot = bot
+ @req = req
+ @res = res
+
+ @method = req.request_method
+ @post = {}
+ if req.body and not req.body.empty?
+ @post = parse_query(req.body)
+ end
+ @args = {}
+ if req.query_string and not req.query_string.empty?
+ @args = parse_query(req.query_string)
+ end
+ @client = req.peeraddr[3]
+
+ # login a botuser with http authentication
+ WEBrick::HTTPAuth.basic_auth(req, res, 'RBotAuth') { |username, password|
+ if username
+ botuser = @bot.auth.get_botuser(Auth::BotUser.sanitize_username(username))
+ if botuser and botuser.password == password
+ @source = botuser
+ true
+ else
+ false
+ end
+ else
+ true # no need to request auth at this point
+ end
+ }
+
+ @path = req.path
+ debug '@path = ' + @path.inspect
+ end
+
+ def parse_query(query)
+ params = CGI::parse(query)
+ params.each_pair do |key, val|
+ params[key] = val.last
+ end
+ params
+ end
+
+ # The target of a RemoteMessage
+ def target
+ @bot
+ end
+
+ # Remote messages are always 'private'
+ def private?
+ true
+ end
+
+ # Sends a response with the specified body, status and content type.
+ def send_response(body, status, type)
+ @res.status = status
+ @res['Content-Type'] = type
+ @res.body = body
+ end
+
+ # Sends a plaintext response
+ def send_plaintext(body, status=200)
+ send_response(body, status, 'text/plain')
+ end
+
+ # Sends a json response
+ def send_json(body, status=200)
+ send_response(body, status, 'application/json')
+ end
+
+ # Sends a html response
+ def send_html(body, status=200)
+ send_response(body, status, 'text/html')
+ end
+ end
+
+ # works similar to a message mapper but for url paths
+ class WebDispatcher
+ class WebTemplate
+ attr_reader :botmodule, :pattern, :options
+ def initialize(botmodule, pattern, options={})
+ @botmodule = botmodule
+ @pattern = pattern
+ @options = options
+ set_auth_path(@options)
+ end
+
+ def recognize(m)
+ message_route = m.path[1..-1].split('/')
+ template_route = @pattern[1..-1].split('/')
+ params = {}
+
+ debug 'web mapping path %s <-> %s' % [message_route.inspect, template_route.inspect]
+
+ message_route.each do |part|
+ tmpl = template_route.shift
+ return false if not tmpl
+
+ if tmpl[0] == ':'
+ # push part as url path parameter
+ params[tmpl[1..-1].to_sym] = part
+ elsif tmpl == part
+ next
+ else
+ return false
+ end
+ end
+
+ debug 'web mapping params is %s' % [params.inspect]
+
+ params
+ end
+
+ def set_auth_path(hash)
+ if hash.has_key?(:full_auth_path)
+ warning "Web route #{@pattern.inspect} in #{@botmodule} sets :full_auth_path, please don't do this"
+ else
+ pre = @botmodule
+ words = @pattern[1..-1].split('/').reject{ |x|
+ x == pre || x =~ /^:/ || x =~ /\[|\]/
+ }
+ if words.empty?
+ post = nil
+ else
+ post = words.first
+ end
+ if hash.has_key?(:auth_path)
+ extra = hash[:auth_path]
+ if extra.sub!(/^:/, "")
+ pre += "::" + post
+ post = nil
+ end
+ if extra.sub!(/:$/, "")
+ if words.length > 1
+ post = [post,words[1]].compact.join("::")
+ end
+ end
+ pre = nil if extra.sub!(/^!/, "")
+ post = nil if extra.sub!(/!$/, "")
+ extra = nil if extra.empty?
+ else
+ extra = nil
+ end
+ hash[:full_auth_path] = [pre,extra,post].compact.join("::")
+ debug "Web route #{@pattern} in #{botmodule} will use authPath #{hash[:full_auth_path]}"
+ end
+ end
+ end
+
+ def initialize(bot)
+ @bot = bot
+ @templates = []
+ end
+
+ def map(botmodule, pattern, options={})
+ @templates << WebTemplate.new(botmodule.to_s, pattern, options)
+ debug 'template route: ' + @templates[-1].inspect
+ return @templates.length - 1
+ end
+
+ # The unmap method for the RemoteDispatcher nils the template at the given index,
+ # therefore effectively removing the mapping
+ #
+ def unmap(botmodule, index)
+ tmpl = @templates[index]
+ raise "Botmodule #{botmodule.name} tried to unmap #{tmpl.inspect} that was handled by #{tmpl.botmodule}" unless tmpl.botmodule == botmodule.name
+ debug "Unmapping #{tmpl.inspect}"
+ @templates[index] = nil
+ @templates.clear unless @templates.compact.size > 0
+ end
+
+ # Handle a web service request, find matching mapping and dispatch.
+ #
+ # In case authentication fails, sends a 401 Not Authorized response.
+ #
+ def handle(m)
+ if @templates.empty?
+ m.send_plaintext('no routes!', 404)
+ return false if @templates.empty?
+ end
+ failures = []
+ @templates.each do |tmpl|
+ # Skip this element if it was unmapped
+ next unless tmpl
+ botmodule = @bot.plugins[tmpl.botmodule]
+ params = tmpl.recognize(m)
+ if params
+ action = tmpl.options[:action]
+ unless botmodule.respond_to?(action)
+ failures << NoActionFailure.new(tmpl, m)
+ next
+ end
+ # check http method:
+ unless not tmpl.options.has_key? :method or tmpl.options[:method] == m.method
+ debug 'request method missmatch'
+ next
+ end
+ auth = tmpl.options[:full_auth_path]
+ debug "checking auth for #{auth.inspect}"
+ # We check for private permission
+ if m.bot.auth.permit?(m.source || Auth::defaultbotuser, auth, '?')
+ debug "template match found and auth'd: #{action.inspect} #{params.inspect}"
+ response = botmodule.send(action, m, params)
+ if m.res.sent_size == 0 and m.res.body.empty?
+ m.send_json(response.to_json)
+ end
+ return true
+ end
+ debug "auth failed for #{auth}"
+ # if it's just an auth failure but otherwise the match is good,
+ # don't try any more handlers
+ m.send_plaintext('Authentication Required!', 401)
+ return false
+ end
+ end
+ failures.each {|r|
+ debug "#{r.template.inspect} => #{r}"
+ }
+ debug "no handler found"
+ m.send_plaintext('No Handler Found!', 404)
+ return false
+ end
+ end
+
+ # Static web dispatcher instance used internally.
+ def web_dispatcher
+ if defined? @web_dispatcher
+ @web_dispatcher
+ else
+ @web_dispatcher = WebDispatcher.new(self)
+ end
+ end
+
+ module Plugins
+ # Mixin for plugins that want to provide a web interface of some sort.
+ #
+ # Plugins include the module and can then use web_map
+ # to register a url to handle.
+ #
+ module WebBotModule
+ # The remote_map acts just like the BotModule#map method, except that
+ # the map is registered to the @bot's remote_dispatcher. Also, the remote map handle
+ # is handled for the cleanup management
+ #
+ def web_map(*args)
+ # stores the handles/indexes for cleanup:
+ @web_maps = Array.new unless defined? @web_maps
+ @web_maps << @bot.web_dispatcher.map(self, *args)
+ end
+
+ # Unregister the remote maps.
+ #
+ def web_cleanup
+ return unless defined? @web_maps
+ @web_maps.each { |h|
+ @bot.web_dispatcher.unmap(self, h)
+ }
+ @web_maps.clear
+ end
+
+ # Redefine the default cleanup method.
+ #
+ def cleanup
+ super
+ web_cleanup
+ end
+ end
+
+ # And just because I like consistency:
+ #
+ module WebCoreBotModule
+ include WebBotModule
+ end
+
+ module WebPlugin
+ include WebBotModule
+ end
+ end
+end # Bot
+end # Irc
+