Class: Autumn::CTCP

Inherits:
Object
  • Object
show all
Defined in:
lib/autumn/ctcp.rb

Overview

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 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.

Constant Summary collapse

CTCP_REQUEST =

Format of an embedded CTCP request.

/\x01(.+?)\x01/
ENCODED_COMMANDS =

CTCP commands whose arguments are encoded according to the CTCP spec (as opposed to other commands, whose arguments are plaintext).

[ 'VERSION', 'PING' ]

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ CTCP

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.



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/autumn/ctcp.rb', line 82

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

Instance Method Details

#added(stem) ⇒ Object

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.)



204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/autumn/ctcp.rb', line 204

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

#ctcp_ping_request(handler, stem, sender, arguments) ⇒ Object

Replies to a CTCP PING request by sending back the same arguments as a PONG reply.



150
151
152
153
# File 'lib/autumn/ctcp.rb', line 150

def ctcp_ping_request(handler, stem, sender, arguments)
  return unless handler == self
  send_ctcp_reply stem, sender[:nick], 'PING', *arguments
end

#ctcp_time_request(handler, stem, sender, arguments) ⇒ Object

Replies to a CTCP TIME request by sending back the local time in RFC-822 format.



158
159
160
161
# File 'lib/autumn/ctcp.rb', line 158

def ctcp_time_request(handler, stem, sender, arguments)
  return unless handler == self
  send_ctcp_reply stem, sender[:nick], 'TIME', Time.now.rfc822
end

#ctcp_version_request(handler, stem, sender, arguments) ⇒ Object

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.



142
143
144
145
# File 'lib/autumn/ctcp.rb', line 142

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

#irc_notice_event(stem, sender, arguments) ⇒ Object

Parses CTCP responses in a NOTICE event.



115
116
117
118
119
120
121
122
123
124
# File 'lib/autumn/ctcp.rb', line 115

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

#irc_privmsg_event(stem, sender, arguments) ⇒ Object

Parses CTCP requests in a PRIVMSG event.



102
103
104
105
106
107
108
109
110
111
# File 'lib/autumn/ctcp.rb', line 102

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

#make_ctcp_message(command, *arguments) ⇒ Object

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.



223
224
225
226
# File 'lib/autumn/ctcp.rb', line 223

def make_ctcp_message(command, *arguments)
  arguments = arguments.map { |arg| quote arg } if ENCODED_COMMANDS.include? command
  "\01#{arguments.unshift(command).join(' ')}\01"
end

#send_ctcp_reply(stem, recipient, command, *arguments) ⇒ Object

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.



173
174
175
176
# File 'lib/autumn/ctcp.rb', line 173

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