Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- require 'socket'
- require 'logger'
- require 'thread'
- require 'numeric'
- require 'handler'
- require 'handler/default_numeric_handler'
- require 'handler/default_ping_handler'
- module Flux
- #
- # Flux::Client is the client interface which connects to the IRC server through
- # a socket and sends/receives messages. It can be used to send users or channels
- # messages, notices or any other information. Flux::Client.connect is an alias
- # and may also be used... it also has a better name.
- #
- class Client
- @@messages = {}
- #
- # Associate certain messages with classes. This method calls `merge!' on the
- # @@messages class variable, so it's not complex. Special messages are
- # :notice_auth, :numeric, and :unknown. So that means if the server sends a
- # NOTICE_AUTH or a NUMERIC or UNKNOWN literal, they won't get handled.
- # Everything else is handled, so PRIVMSG is associated with :privmsg, and so
- # on. Pass a hash of key/value pairs to this method (:privmsg => Privmsg).
- #
- def self.register_messages(messages = {})
- @@messages.merge!(messages)
- end
- require 'messages'
- attr_reader :address, :port, :debug, :mutex, :logger, :buffer, :threads
- attr_accessor :handlers
- alias_method :debug? , :debug
- #
- # Initialize a new client object. `address' is the server address, for example
- # a string like 'irc.freenode.net', and `attr' is a hash of pre-defined
- # attributes. The predefined attributes are:
- #
- # :port => A fixnum port number
- # :debug => A boolean whether to enable debug output
- # :logdev => Log device, such as STDOUT for the Logger instance
- # :nick => Nickname to register as
- # :user => Username
- # :real => Supposedly your real name
- # :pass => The server password (PASS command)
- #
- # This method also has an alias, Flux::Client.connect. Alternatively, when
- # initializing a client object, you can pass a block which passes the client
- # object as the block parameter and assumes Flux::Client#process! and
- # Flux::Client#disconnect! at the end of the block.
- #
- #
- # <b>Flux::Client#threads</b>
- #
- #
- # Flux::Client#threads is an array of threads that the client spawns.
- # Flux::Client.new does some tricky stuff to this array; It adds a method
- # to the metaclass called `purge!' which removes all dead threads.
- #
- # This note is kept here to avoid any confusion or debugging hassles to
- # people extending or modifying the client library.
- #
- def initialize(address, attr = {})
- @mutex = Mutex.new
- @address = address
- @port = (attr[:port] || 6667).to_i
- @debug = attr[:debug] ? true : false
- @logger = Logger.new(attr[:logdev] || STDOUT)
- @buffer = []
- @handlers = []
- @threads = []
- @threads.instance_variable_set('@logger', @logger)
- @threads.instance_variable_set('@client', self)
- # Default handlers.
- @handlers << Handler::DefaultNumericHandler.new
- @handlers << Handler::DefaultPingHandler.new
- # Let's abort when a thread fails.
- Thread.abort_on_exception = true
- class << @threads
- def purge!
- dead = find_all { |thread| !thread.alive? }
- if @client.debug?
- @logger.debug("Purging threads array: #{dead.size} thread(s) removed, #{size - dead.size} still alive")
- end
- unless dead.empty?
- for thread in dead
- delete(thread)
- end
- return self
- end
- end
- alias_method :purge, :purge!
- end
- @logger.debug("Attempting to connect to #@address:#@port") if @debug
- @socket = TCPSocket.new(@address, @port)
- @logger.debug("You are now connected to #@address:#@port") if @debug
- # Automatic login?
- login(attr[:nick], attr[:user] || attr[:nick], attr[:real] || attr[:nick]) if attr[:nick]
- if block_given?
- @logger.debug('Initializing client object via block form') if @debug
- yield self
- process!
- disconnect!
- end
- end
- #
- # Disconnect from the server. It does nothing if you're not connected in the
- # first place. Flux::Client#disconnect! is an alias.
- #
- def disconnect
- if connected?
- @socket.close unless @socket.closed?
- @logger.debug('Disconnected') if @debug
- else
- @logger.debug('Disconnect attempted, but not connected') if @debug
- end
- end
- #
- # This is a convenience method for checking the connection status between the
- # client and the server. It'll return true if the client is still connected
- # and false if it isn't.
- #
- def connected?
- @socket ? true : false
- end
- #
- # Write data to the socket.
- #
- def write(msg)
- @logger.debug("OUTGOING: #{msg.strip}") if @debug
- @socket.write("#{msg}\r\n")
- end
- #
- # Login to the IRC server.
- #
- def login(nick, user = nick, real = nick, pass = nil)
- nick(nick)
- user(user, real)
- pass(pass) if pass
- end
- #
- # Send NICK command.
- #
- def nick(nickname = nil)
- nickname ? _write("NICK #{nickname}") : write('NICK')
- end
- #
- # Send USER command.
- #
- def user(username, realname)
- _write("USER #{username} #{username} #{username} :#{realname}")
- end
- #
- # Send PASS command.
- #
- def pass(password)
- _write("PASS #{password}")
- end
- #
- # Send PRIVMSG command.
- #
- def privmsg(target, text)
- for line in text.split("\n")
- next if line.strip.empty?
- write("PRIVMSG #{target} :#{line}")
- end
- end
- #
- # Send NOTICE command.
- #
- def notice(target, text)
- write("NOTICE #{target} :#{text}")
- end
- #
- # Send PING command.
- #
- def ping(data)
- write("PING :#{data}")
- end
- #
- # Send PONG command.
- #
- def pong(data)
- write("PONG :#{data}")
- end
- #
- # Process incoming data infinitely and sort them into their corresponding message
- # objects such that a PRIVMSG is a PrivateMessage object, a NOTICE is a Notice
- # object, and so on. Flux::Client#main is an alias.
- #
- def process!
- @logger.debug('Entering main processing loop') if @debug
- loop do
- rw, wr, err = select([@socket], [@socket], [@socket])
- read unless rw.empty?
- write unless wr.empty?
- @logger.error("Socket error") if @debug and !err.empty?
- end
- end
- class << self
- alias_method :connect, :new
- end
- alias_method :disconnect!, :disconnect
- alias_method :main, :process!
- #
- # See if the connection has been registered by the server. This means it's safe
- # to send messages.i
- #
- # This method by default will return false, but is redefined by the handler
- # Flux::Handler::DefaultNumericHandler when it receives an RPL_WELCOME.
- #
- def registered?
- false
- end
- #
- # This method is invoked when the socket is writable or can be
- # invoked when you want to write data to the socket.
- #
- def write(data= nil)
- @buffer << data if data
- @mutex.synchronize do
- if registered?
- for line in @buffer
- _write(line)
- end
- @buffer.clear
- end
- end
- end
- private
- #
- # This method is invoked when the socket is readable, and it grabs
- # a line of data from the server and processes it in its own thread.
- #
- def read
- data = @socket.gets.strip
- @logger.debug("INCOMING << #{data}") if @debug
- invoke_handlers(parse_message(data))
- end
- #
- # This method is like Flux::Client#write, except it bypasses any
- # buffer queueing and writes straight to the socket.
- #
- def _write(data)
- @logger.debug("OUTGOING >> #{data}") if @debug
- @socket.write("#{data}\r\n")
- end
- #
- # Parse a line into its corresponding Flux::Message object.
- #
- def parse_message(raw)
- origin, line = raw.match(/^(?::(\S ) )?(. )$/).to_a[1..-1]
- case line
- # parse NOTICE AUTH messages
- when /^NOTICE AUTH :(. )$/
- command = :notice_auth
- message = @@messages[:notice_auth].new(self, raw, command, [], $1)
- when /^(\d ) (\S )(?: (.*?))(?: :(. ))?$/
- command = :numeric
- arguments = $3 ? $3.split(' ') : []
- message = @@messages[:numeric].new(self, raw, command, arguments, $4)
- message.numeric = $1.to_i
- message.my_nick = $2
- when /^(\S )(?: (.*?))(?: ?:(. ))?$/
- command = $1.downcase.to_sym
- arguments = $2 ? $2.split(' ') : []
- message = @@messages[command].new(self, raw, command, arguments, $3)
- else
- command = :unknown
- message = @@messages[:unknown].new(self, raw, command, [], nil)
- end
- @logger.debug("Parsed (#{message.class.name.split('::')[-1]}): #{raw.inspect}") if @debug
- message.origin = OpenStruct.new
- if parsed = parse_origin(origin)
- message.origin.nick = parsed[0]
- message.origin.user = parsed[1]
- message.origin.host = parsed[2]
- else
- message.origin.server = origin
- end
- message.customize
- message
- end
- #
- # Parse an origin and return an array of [nick, user, host] if the format is
- # valid. Otherwise return nil.
- #
- def parse_origin(origin)
- [$1, $2, $3] if origin =~ /^([^!] )(?:!([^@] )(?:@(. )))?$/
- end
- #
- # Determine if a given name is a channel name.
- #
- def channel?(name)
- chantypes = @isupport ? isupport[:chantypes] : '#'
- if chantypes =~ Regexp.new(Regexp.escape(name[0..0])) then true else false end
- end
- #
- # Invoke handlers which correspond to a given message's signal.
- #
- def invoke_handlers(message)
- if handlers = @handlers.find_all { |h| h.signals.include?(message.command) }
- command = case message.command
- when :notice_auth then 'NOTICE AUTH message'
- when :numeric then "numeric message (#{Reply[message.numeric]}: #{message.numeric})"
- when :unknown then 'unknown message'
- else message.command.to_s.upcase
- end
- @logger.debug("Invoking #{handlers.size} handler(s) for #{command}") if @debug
- for handler in handlers
- @threads << thread = Thread.new { handler.call(self, message) }
- thread.join unless handler.threaded?
- end
- end
- @threads.purge!
- end
- end
- end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement