require 'repp' require "mobb/version" module Mobb class Matcher def initialize(pattern, options) @pattern, @options = pattern, options; end def regexp?; pattern.is_a?(Regexp); end def inspect; "pattern: #{@pattern}, options #{@options}"; end def invoke(time = Time.now) @options[:last_invoked] = time; end def match?(context) case context when String string_matcher(context) when Time # TODO: do something false when Array context.all? { |c| match?(c) } else false end end class Matched attr_reader :pattern, :matched def initialize(pattern, matched) @pattern, @matched = pattern, matched; end end def pattern; @pattern; end def string_matcher(string) case pattern when Regexp if res = pattern.match(string) Matched.new(pattern, res.captures) else false end when String @options[:laziness] ? string.include?(pattern) : string == pattern else false end end end class Base def call(env) dup.call!(env) end def tick(env) dup.tick!(env) end def call!(env) @env = env invoke { dispatch! } [@body, @attachments] end def tick!(env) fail # TODO: write logic here end def dispatch! # TODO: encode input messages invoke do # TODO: before filters handle_event end ensure # TODO: after fillters end def invoke res = catch(:halt) { yield } return if res.nil? res = [res] if String === res if Array === res && String === res.first tmp = res.dup @body = tmp.shift @attachments = tmp.pop else @attachments = res end nil end def handle_event(base = settings, passed_block = nil) if responds = base.events[@env.event_type] responds.each do |pattern, block, conditions| process_event(pattern, conditions) do |*args| event_eval { block[*args] } end end end # TODO: Define respond missing if receive reply message nil end def process_event(pattern, conditions, block = nil, values = []) res = pattern.match?(@env.body) catch(:pass) do conditions.each { |c| throw :pass unless c.bind(self).call } case res when ::Mobb::Matcher::Matched yield(self, *(res.matched)) when TrueClass yield self else nil end end end def event_eval; throw :halt, yield; end def settings self.class.settings end class << self CALLERS_TO_IGNORE = [ /\/mobb(\/(base|main|show_exceptions))?\.rb$/, # all sinatra code /^\(.*\)$/, # generated code /rubygems\/(custom|core_ext\/kernel)_require\.rb$/, # rubygems require hacks /active_support/, # active_support require hacks /bundler(\/runtime)?\.rb/, # bundler require hacks /<internal:/, # internal in ruby >= 1.9.2 /src\/kernel\/bootstrap\/[A-Z]/ # maglev kernel files ] attr_reader :events def reset! @events = {} @conditions = [] end def settings self end def receive(pattern, options = {}, &block) event(:message, pattern, options, &block); end alias :on :receive #def every(pattern, options = {}, &block) event(:cron, pattern, options, &block); end def event(type, pattern, options, &block) (@events[type] ||= []) << compile!(type, pattern, options, &block) end def compile!(type, pattern, options, &block) options.each_pair { |option, args| send(option, *args) } matcher = compile(pattern, options) unbound_method = generate_method("#{type}", &block) conditions, @conditions = @conditions, [] wrapper = block.arity != 0 ? proc { |instance, args| unbound_method.bind(instance).call(*args) } : proc { |instance, args| unbound_method.bind(instance).call } [ matcher, wrapper, conditions ] end def compile(pattern, options) Matcher.new(pattern, options); end def generate_method(name, &block) define_method(name, &block) method = instance_method(name) remove_method(name) method end def helpers(*extensions, &block) class_eval(&block) if block_given? include(*extensions) if extensions.any? end def development?; environment == :development; end def production?; environment == :production; end def test?; environment == :test; end def set(option, value = (not_set = true), ignore_setter = false, &block) raise ArgumentError if block && !not_set value, not_set = block, false if block if not_set raise ArgumentError unless option.respond_to?(:each) option.each { |k,v| set(k,v) } return self end setter_name = "#{option}=" if respond_to?(setter_name) && ! ignore_setter return __send__(setter_name, value) end setter = proc { |val| set(option, val, true) } getter = proc { value } case value when Proc getter = value when Symbol, Integer, FalseClass, TrueClass, NilClass getter = value.inspect when Hash setter = proc do |val| val = value.merge(val) if Hash === val set(option, val, true) end end define_singleton(setter_name, setter) define_singleton(option, getter) define_singleton("#{option}?", "!!#{option}") unless method_defined?("#{option}?") self end def condition(name = "#{caller.first[/`.*'/]} condition", &block) @conditions << generate_method(name, &block) end def ignore_bot(cond) condition do @env.bot? != cond end end def reply_to_me(cond) condition do @env.reply_to.include?(settings.name) == cond end end def enable(*options) options.each { |option| set(option, true) }; end def disable(*options) options.each { |option| set(option, false) }; end def clear(*options) options.each { |option| set(option, nil) }; end def run!(options = {}, &block) return if running? set options handler = detect_repp_handler handler_name = handler.name.gsub(/.*::/, '') service_settings = settings.respond_to?(:service_settings) ? settings.service_settings : {} begin start_service(handler, service_settings, handler_name, &block) rescue => e $stderr.puts e.message $stderr.puts e.backtrace ensure quit! end end def quit! return unless running? running_service.respond_to?(:stop!) ? running_service.stop! : running_service.stop $stderr.puts "== Great sound Mobb, thank you so much" clear :running_service, :handler_name end def running? running_service? end private def start_service(handler, service_settings, handler_name) handler.run(self, service_settings) do |service| $stderr.puts "== Mobb (v#{Mobb::VERSION}) is in da house with #{handler_name}. Make some noise!" setup_traps set running_service: service set handler_name: handler_name yield service if block_given? end end def setup_traps if traps? at_exit { quit! } [:INT, :TERM].each do |signal| old_handler = Signal.trap(signal) do quit! old_handler.respond_to?(:call) ? old_handler.call : exit end end disable :traps end end def detect_repp_handler services = Array(service) services.each do |service_name| begin return Repp::Handler.get(service_name.to_s) rescue LoadError, NameError end end fail "Service handler (#{services.join(',')}) not found" end def define_singleton(name, content = Proc.new) singleton_class.class_eval do undef_method(name) if method_defined?(name) String === content ? class_eval("def #{name}() #{content}; end") : define_method(name, &content) end end def caller_files cleaned_caller(1).flatten end def cleaned_caller(keep = 3) caller(1). map! { |line| line.split(/:(?=\d|in )/, 3)[0,keep] }. reject { |file, *_| CALLERS_TO_IGNORE.any? { |pattern| file =~ pattern } } end def inherited(subclass) subclass.reset! subclass.set :app_file, caller_files.first unless subclass.app_file? super end end reset! set :name, 'mobb' set :environment, (ENV['APP_ENV'] || ENV['REPP_ENV'] || :development).to_sym disable :run, :quiet clear :running_service, :handler_name enable :traps set :service, %w[shell] clear :app_file end class Application < Base set :logging, Proc.new { !test? } set :run, Proc.new { !test? } clear :app_file def self.register(*extensions, &block) added_methods = extensions.flat_map(&:public_instance_methods) Delegator.delegate(*added_methods) super(*extensions, &block) end end module Delegator #:nodoc: def self.delegate(*methods) methods.each do |method_name| define_method(method_name) do |*args, &block| return super(*args, &block) if respond_to? method_name Delegator.target.send(method_name, *args, &block) end private method_name end end delegate :receive, :on, #:every, :set, :enable, :disable, :clear, :helpers class << self attr_accessor :target end self.target = Application end end