# Defines the Autum::CTCP class, which implements CTCP support. require 'time' module Autumn # A listener for a Stem that listens for and handles CTCP requests. You can # add CTCP support for your IRC client by instantiating this object and # passing it to the Stem#add_listener method. # # CTCP stands for Client-to-Client Protocol and is a way that IRC clients and # servers can request and transmit more information about each other. Modern # IRC clients all have CTCP support, and many servers expect or assume that # their clients support CTCP. CTCP is also used as a basis for further # extensions to IRC, such as DCC and XDCC. # # This class implements the spec defined at http://www.invlogic.com/irc/ctcp.html # # Because some IRC servers will disconnect clients that send a large number of # messages in a short period of time, this listener will only send one CTCP # reply per second, with up to a maximum of 10 replies waiting in the queue # (after which new requests are ignored). These values can be adjusted in the # initialization options. # # This class acts as a listener plugin: Any of the methods specified below can # be implemented by any other listener, and will be invoked by this listener # when appropriate. # # To respond to incoming CTCP requests, you should implement methods of the # form ctcp_*_request, where "*" is replaced with the lowercase name # of the CTCP command. (For example, to handle VERSION requests, implement # +ctcp_version_request+). This method will be invoked whenever a request is # received by the IRC client. It will be given the following parameters: # # 1. the CTCP instance that parsed the request, # 2. the Stem instance that received the request, # 3. the person who sent the request (a hash in the form of that used by Stem; # see Stem#add_listener for more information), and # 4. an array of arguments passed along with the request. # # In addition, you can implement +ctcp_request_received+, which will then be # invoked for any and all incoming CTCP requests. It is passed the following # arguments: # # 1. the name of the request, as a lowercase symbol, # 2. the CTCP instance that parsed the request, # 3. the Stem instance that received the request, # 4. the person who sent the request (a sender hash -- see the Leaf docs), and # 5. an array of arguments passed along with the request. # # This class will by default respond to some incoming CTCP requests and # generate appropriate replies; however, it does not implement any specific # behavior for parsing incoming replies. If you wish to parse replies, you # should implement methods in your listener of the form # ctcp_*_response, with the "*" character replaced as above. This # method will be invoked whenever a reply is received by this listener. You # can also implement ctcp_response_received just as above. The # parameters for these methods are the same as those listed above. # # Responses are assumed to be any CTCP messages that are sent as a NOTICE (as # opposed to a PRIVMSG). Because they are NOTICEs, your program should not # send a message in response. # # In addition to responding to incoming CTCP requests and replies, your # listener can use its stem to send CTCP requests and replies. See the added # method for more detail. class CTCP # Format of an embedded CTCP request. CTCP_REQUEST = /\x01(.+?)\x01/ # CTCP commands whose arguments are encoded according to the CTCP spec (as # opposed to other commands, whose arguments are plaintext). ENCODED_COMMANDS = [ 'VERSION', 'PING' ] # Creates a new CTCP parser. Options are: # # +reply_queue_size+:: The maximum number of pending replies to store in the # queue, after which new CTCP requests are ignored. # +reply_rate+:: The minimum time, in seconds, between consecutive CTCP # replies. def initialize(options={}) @options = options @options[:reply_queue_size] ||= 10 @options[:reply_rate] ||= 0.25 @reply_thread = Hash.new @reply_queue = Hash.new do |hsh, key| hsh[key] = ForgetfulQueue.new(@options[:reply_queue_size]) @reply_thread[key] = Thread.new(key) do |stem| loop do #TODO wake thread when stem is quitting so this thread can terminate? reply = @reply_queue[stem].pop stem.notice reply[:recipient], reply[:message] sleep @options[:reply_rate] end end hsh[key] end end # Parses CTCP requests in a PRIVMSG event. def irc_privmsg_event(stem, sender, arguments) # :nodoc: arguments[:message].scan(CTCP_REQUEST).flatten.each do |ctcp| ctcp_args = ctcp.split(' ') request = ctcp_args.shift ctcp_args = ctcp_args.map { |arg| unquote arg } if ENCODED_COMMANDS.include? request meth = "ctcp_#{request.downcase}_request".to_sym stem.broadcast meth, self, stem, sender, ctcp_args stem.broadcast :ctcp_request_received, request.downcase.to_sym, self, stem, sender, ctcp_args end end # Parses CTCP responses in a NOTICE event. def irc_notice_event(stem, sender, arguments) # :nodoc: arguments[:message].scan(CTCP_REQUEST).flatten.each do |ctcp| ctcp_args = ctcp.split(' ') request = ctcp_args.shift ctcp_args = ctcp_args.map { |arg| unquote arg } if ENCODED_COMMANDS.include? request meth = "ctcp_#{request.downcase}_response".to_sym stem.broadcast meth, self, stem, sender, ctcp_args stem.broadcast :ctcp_response_received, request.downcase.to_sym, self, stem, sender, ctcp_args end end # Replies to a CTCP VERSION request by sending: # # * the name of the IRC client ("Autumn, a Ruby IRC framework"), # * the operating system name and version, and # * the home page URL for Autumn. # # To determine the OS name and version, this method runs the uname # -sr command; if your operating system does not support this command, # you should override this method. # # Although the CTCP spec states that the VERSION response should be three # encoded strings (as shown above), many modern clients expect one plaintext # string. If you'd prefer compatibility with those clients, you should # override this method to return a single plaintext string and remove the # VERSION command from +ENCODED_COMMANDS+. def ctcp_version_request(handler, stem, sender, arguments) return unless handler == self send_ctcp_reply stem, sender[:nick], 'VERSION', "Autumn #{AUTUMN_VERSION}, a Ruby IRC framework", `uname -sr`, "http://github.com/RISCfuture/autumn" end # Replies to a CTCP PING request by sending back the same arguments as a # PONG reply. def ctcp_ping_request(handler, stem, sender, arguments) return unless handler == self send_ctcp_reply stem, sender[:nick], 'PING', *arguments end # Replies to a CTCP TIME request by sending back the local time in RFC-822 # format. def ctcp_time_request(handler, stem, sender, arguments) return unless handler == self send_ctcp_reply stem, sender[:nick], 'TIME', Time.now.rfc822 end # Adds a CTCP reply to the queue. You must pass the Stem instance that will # be sending this reply, the recipient (channel or nick), and the name of # the CTCP command (as an uppercase string). Any additional arguments are # taken to be arguments of the CTCP reply, and are thus encoded and joined # by space characters, as specified in the CTCP white paper. The arguments # should all be strings. # # +recipient+ can be a nick, a channel name, or a sender hash, as necessary. # Encoding of arguments is only done for commands in +ENCODED_COMMANDS+. def send_ctcp_reply(stem, recipient, command, *arguments) recipient = recipient[:nick] if recipient.kind_of? Hash @reply_queue[stem] << { :recipient => recipient, :message => make_ctcp_message(command, *arguments) } end # When this listener is added to a stem, the stem gains the ability to send # CTCP messages directly. Methods of the form ctcp_*, where "*" is # the lowercase name of a CTCP action, will be forwarded to this listener, # which will send the CTCP message. The first parameter of the method is the # nick of one or more recipients; all other parameters are parameters for # the CTCP command. See the CTCP spec for more information on the different # commands and parameters available. # # For example, to send an action (such as "/me is cold") to a channel: # # stem.ctcp_action "#channel", "is cold" # # In addition, the stem gains the ability to send CTCP replies. Replies are # messages that are added to this class's reply queue, adding flood abuse # prevention. To send a reply, call a Stem method of the form # ctcp_reply_*, where "*" is the command name you are replying to, # in lowercase. Pass first the nick or sender hash of the recipient, then # any parameters as specified by the CTCP spec. For example, to respond to a # CTCP VERSION request: # # stem.ctcp_reply_version sender, 'Bot Name', 'Computer Name', 'Other Info' # # (Note that responding to VERSION requests is already handled by this class # so you'll need to either override or delete the ctcp_version_request # method to do this.) def added(stem) stem.instance_variable_set :@ctcp, self class << stem def method_missing(meth, *args) if meth.to_s =~ /^ctcp_reply_([a-z]+)$/ then @ctcp.send_ctcp_reply self, args.shift, $1.to_s.upcase, *args elsif meth.to_s =~ /^ctcp_([a-z]+)$/ then privmsg args.shift, @ctcp.make_ctcp_message($1.to_s.upcase, *args) else super end end end end # Creates a CTCP-formatted message with the given command (uppercase string) # and arguments (strings). The string returned is suitable for transmission # over IRC using the PRIVMSG command. def make_ctcp_message(command, *arguments) arguments = arguments.map { |arg| quote arg } if ENCODED_COMMANDS.include? command "\01#{arguments.unshift(command).join(' ')}\01" end private def quote(str) chars = str.split('') chars.map! do |char| case char when "\0" then '\0' when "\1" then '\1' when "\n" then '\n' when "\r" then '\r' when " " then '\@' when "\\" then '\\\\' else char end end return chars.join end def unquote(str) str.gsub('\\\\', '\\').gsub('\@', " ").gsub('\r', "\r").gsub('\n', "\n").gsub('\1', "\1").gsub('\0', "\0") end end end