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