lib/marvin/irc/client.rb in Sutto-marvin-0.4.0 vs lib/marvin/irc/client.rb in Sutto-marvin-0.8.0.0

- old
+ new

@@ -1,139 +1,167 @@ require 'eventmachine' module Marvin::IRC - - # == Marvin::IRC::Client - # An EventMachine protocol implementation built to - # serve as a basic, single server IRC client. - # - # Operates on the principal of Events as well - # as handlers. - # - # === Events - # Events are things that can happen (e.g. an - # incoming message). All outgoing events are - # automatically handled from within the client - # class. Incoming events are currently based - # on regular expression based matches of - # incoming messages. the Client#register_event - # method takes either an instance of Marvin::IRC::Event - # or a set of arguments which will then be used - # in the constructor of a new Marvin::IRC::Event - # instance (see, for example, the source code for - # this class for examples). - # - # === Handlers - # Handlers on the other hand do as the name suggests - # - they listen for dispatched events and act accordingly. - # Handlers are simply objects which follow a certain - # set of guidelines. Typically, a handler will at - # minimum respond to #handle(event_name, details) - # where event_name is a symbol for the current - # event (e.g. :incoming_event) whilst details is a - # a hash of details about the current event (e.g. - # message target and the message itself). - # - # ==== Getting the current client instance - # If the object responds to client=, The client will - # call it with the current instance of itself - # enabling the handler to do things such as respond. - # Also, if a method handle_[message_name] exists, - # it will be called instead of handle. - # - # ==== Adding handlers - # To add an object as a handler, you simply call - # the class method, register_handler with the - # handler as the only argument. class Client < Marvin::AbstractClient - cattr_accessor :stopped - self.stopped = false + @@stopped = false + attr_accessor :em_connection class EMConnection < EventMachine::Protocols::LineAndTextProtocol - attr_accessor :client, :server, :port + is :loggable + attr_accessor :client, :port, :configuration + def initialize(*args) - opts = args.extract_options! + @configuration = args.last.is_a?(Marvin::Nash) ? args.pop : Marvin::Nash.new super(*args) - self.client = Marvin::IRC::Client.new(opts) - self.client.em_connection = self + @client = Marvin::IRC::Client.new(@configuration) + @client.em_connection = self + @connected = false + rescue Exception => e + Marvin::ExceptionTracker.log(e) + Marvin::IRC::Client.stop end def post_init - client.process_connect super + if should_use_ssl? + logger.info "Starting SSL for #{host_with_port}" + start_tls + else + connected! + end + rescue Exception => e + Marvin::ExceptionTracker.log(e) + Marvin::IRC::Client.stop end + + def ssl_handshake_completed + logger.info "SSL handshake completed for #{host_with_port}" + connected! if should_use_ssl? + rescue Exception => e + Marvin::ExceptionTracker.log(e) + Marvin::IRC::Client.stop + end def unbind - client.process_disconnect + @client.process_disconnect super end def receive_line(line) - Marvin::Logger.debug "<< #{line.strip}" - self.client.receive_line(line) + return unless @connected + line = line.strip + logger.debug "<< #{line}" + @client.receive_line(line) + rescue Exception => e + logger.warn "Uncaught exception raised; Likely in Marvin" + Marvin::ExceptionTracker.log(e) end - end - - def send_line(*args) - args.each { |line| Marvin::Logger.debug ">> #{line.strip}" } - em_connection.send_data *args - end - - ## Client specific details - - # Starts the EventMachine loop and hence starts up the actual - # networking portion of the IRC Client. - def self.run(force = false) - return if self.stopped && !force - self.setup # So we have options etc - settings = YAML.load_file(Marvin::Settings.root / "config/connections.yml") - if settings.is_a?(Hash) - # Use epoll if available - EventMachine.epoll - EventMachine::run do - settings.each do |name, options| - settings = options.symbolize_keys! - settings[:server] ||= name - settings.reverse_merge!(:port => 6667, :channels => []) - connect settings - end + def send_line(*lines) + return unless @connected + lines.each do |line| + logger.debug ">> #{line.strip}" + send_data line end - else - logger.fatal "config/connections.yml couldn't be loaded. Exiting" end + + def connected! + @connected = true + @client.process_connect + end + + def should_use_ssl? + @should_use_ssl ||= @configuration.ssl? + end + + def host_with_port + "#{@configuration.host}:#{@configuration.port}" + end + end - def self.connect(opts = {}) - logger.info "Connecting to #{opts[:server]}:#{opts[:port]} - Channels: #{opts[:channels].join(", ")}" - EventMachine::connect(opts[:server], opts[:port], EMConnection, opts) - end + ## Client specific details - def self.stop - return if self.stopped - logger.debug "Telling all connections to quit" - self.connections.dup.each { |connection| connection.quit } - logger.debug "Telling Event Machine to Stop" - EventMachine::stop_event_loop - logger.debug "Stopped." - self.stopped = true + def send_line(*args) + @em_connection.send_line(*args) end - def self.add_reconnect(opts = {}) - Marvin::Logger.warn "Adding entry to reconnect to #{opts[:server]}:#{opts[:port]} in 15 seconds" - EventMachine::add_timer(15) do - Marvin::Logger.warn "Attempting to reconnect to #{opts[:server]}:#{opts[:port]}" - Marvin::IRC::Client.connect(opts) + class << self + + # Starts the EventMachine loop and hence starts up the actual + # networking portion of the IRC Client. + def run(opts = {}, force = false) + self.development = opts[:development] + return if @stopped && !force + self.setup # So we have options etc + connections_file = Marvin::Settings.root / "config" / "connections.yml" + connections = Marvin::Nash.load_file(connections_file) rescue nil + if connections.present? + # Use epoll if available + EventMachine.kqueue + EventMachine.epoll + EventMachine.run do + connections.each_pair do |server, configuration| + connect(configuration.merge(:server => server.to_s)) + end + Marvin::Distributed::Server.start if Marvin::Distributed::Handler.registered? + @@stopped = false + end + else + logger.fatal "config/connections.yml couldn't be loaded." + end end + + def connect(c, &blk) + c = normalize_connection_options(c) + raise ArgumentError, "Your connection options must specify a server" if !c.server? + raise ArgumentError, "Your connection options must specify a port" if !c.port? + real_block = blk.present? ? proc { |c| blk.call(connection.client) } : nil + logger.info "Connecting to #{c.server}:#{c.port} (using ssl: #{c.ssl?}) - Channels: #{c.channels.join(", ")}" + EventMachine.connect(c.server, c.port, EMConnection, c, &real_block) + end + + def stop + return if @@stopped + logger.debug "Telling all connections to quit" + connections.dup.each { |connection| connection.quit } + logger.debug "Telling Event Machine to Stop" + EventMachine.stop_event_loop + logger.debug "Stopped." + @@stoped = true + end + + def add_reconnect(c) + logger.warn "Adding timer to reconnect to #{c.server}:#{c.port} in 15 seconds" + EventMachine.add_timer(15) do + logger.warn "Preparing to reconnect to #{c.server}:#{c.port}" + connect(c) + end + end + + protected + + def normalize_connection_options(config) + config = Marvin::Nash.new(config) if !config.is_a?(Marvin::Nash) + config = config.normalized + channels = config.channels + if channels.present? + config.channels = [*channels].compact.reject { |c| c.blank? } + else + config.channels = [] + end + config.host ||= config.server + return config + end + end # Registers a callback handle that will be periodically run. def periodically(timing, event_callback) - callback = proc { self.dispatch event_callback.to_sym } - EventMachine::add_periodic_timer(timing, &callback) + EventMachine.add_periodic_timer(timing) { dispatch(event_callback.to_sym) } end end end \ No newline at end of file