Class: Autumn::Stem

Inherits:
Object
  • Object
show all
Includes:
Anise::Annotation, StemFacade
Defined in:
lib/autumn/stem.rb

Overview

A connection to an IRC server. The stem acts as the IRC client on which a Leaf runs. It receives messages from the IRC server and sends messages to the server. Stem is compatible with many IRC daemons; details of the IRC protocol are handled by a Daemon instance. (See “Compatibility with Different Server Types,” below).

Generally stems are initialized by the Foliater, but should you want to instantiate one yourself, a Stem is instantiated with a server to connect to and a nickname to acquire (see the initialize method docs). Once you initialize a Stem, you should call add_listener one or more times to indicate to the stem what objects are interested in working with it.

Listeners and Listener Plug-Ins

An object that functions as a listener should conform to an implicit protocol. See the add_listener docs for more infortmation on what methods you can implement to listen for IRC events. Duck typing is used – you need not implement every method of the protocol, only those you are concerned with.

Listeners can also act as plugins: Such listeners add functionality to other listeners (for example, a CTCP listener that adds CTCP support to other listeners, such as a Leaf instance). For more information, see the add_listener docs.

Starting the IRC Session

Once you have finished configuring your stem and you are ready to begin the IRC session, call the start method. This method blocks until the the socket has been closed, so it should be run in a thread. Once the connection has been made, you are free to send and receive IRC commands until you close the connection, which is done with the quit method.

Receiving and Sending IRC Commands

Receiving events is explained in the add_listener docs. To send an IRC command, simply call a method named after the command name. For instance, if you wish to PRIVMSG another nick, call the privmsg method. If you wish to JOIN a channel, call the join method. The parameters should be specified in the same order as the IRC command expects.

For more information on what IRC commands are “method-ized”, see the IRC_COMMANDS constant. For more information on the proper way to use these commands (and thus, the methods that call them), consult the Daemon class.

Compatibility with Different Server Types

Many different IRC server daemons exist, and each one has a slightly different IRC implementation. To manage this, there is an option called server_type, which is set automatically by the stem if it can determine the IRC software that the server is running. Server types are instances of the Daemon class, and are associated with a name. A stem’s server type affects things like response codes, user modes, and channel modes, as these vary from server to server.

If the stem is unsure what IRC daemon your server is running, it will use the default Daemon instance. This default server type will be compatible with nearly every server out there. You may not be able to leverage some of the more esoteric IRC features of your particular server, but for the most common uses of IRC (sending and receiving messages, for example), it will suffice.

If you’d like to manually specify a server type, you can pass its name for the server_type initialization option. Consult the resources/daemons directory for valid Daemon names and hints on how to make your own Daemon specification, should you desire.

Channel Names

The convention for Autumn channel names is: When you specify a channel to an Autumn stem, you can (but don’t have to) prefix it with the ‘#’ character, if it’s a normal IRC channel. When an Autumn stem gives a channel name to you, it will always start with the ‘#’ character (assuming it’s a normal IRC channel, of course). If your channel is prefixed with a different character (say, ‘&’), you will need to include that prefix every time you pass a channel name to a stem method.

So, if you would like your stem to send a message to the “##kittens” channel, you can omit the ‘#’ character; but if it’s a server-local channel called “&kittens”, you will have to provide the ‘&’ character. Likewise, if you are overriding a hook method, you can be guaranteed that the channel given to you will always be called “##kittens”, and not “kittens”.

Synchronous Methods

Because new messages are received and processed in separate threads, methods can sometimes receive messages out of order (for instance, if a first message takes a long time to process and a second message takes a short time to process). In the event that you require a guarantee that your method will receive messages in order, and that it will only be invoked in a single thread, annotate your method with the stem_sync property.

For instance, you might want to ensure that you are finished processing 353 messages (replies to NAMES commands) before you tackle 366 messages (end of NAMES list). To ensure these methods are invoked in the correct order:

class MyListener
  def irc_rpl_namreply_response(stem, sender, recipient, arguments, msg)
    [...]
  end

  def irc_rpl_endofnames_response(stem, sender, recipient, arguments, msg)
    [...]
  end

  ann :irc_rpl_namreply_response, :stem_sync => true
  ann :irc_rpl_endofnames_response, :stem_sync => true
end

All such methods will be run in a single thread, and will receive server messages in order. Because of this, it is important that synchronized methods do not spend a lot of time processing a single message, as it forces all other synchronous methods to wait their turn.

This annotation is only relevant to “invoked” methods, those methods in listeners that are invoked by the stem’s broadcast method. Methods that are marked with this annotation will also run faster, because they don’t have the overhead of setting up a new thread.

Many of Stem’s own internal methods are synchronized, to ensure internal data such as the channels list and channel members list stays consistent. Because of this, any method marked as synchronized can be guaranteed that the stem’s channel data is consistent and “in sync” for the moment of time that the message was received.

Throttling

If you send a message with the privmsg command, it will not be throttled. (Most IRC servers have some form of flood control that throttles rapid privmsg commands, however.)

If your IRC server does not have flood control, or you want to use client-side flood control, you can enable the throttling option. The stem will throttle large numbers of simultaneous messages, sending them with short pauses in between.

The privmsg command will still not be throttled (since it is a facade for the pure IRC command), but the StemFacade#message command will gain the ability to throttle its messages.

By default, the stem will begin throttling when there are five or more messages queued to be sent. It will continue throttling until the queue is emptied. When throttling, messages will be sent with a delay of one second between them. These options can be customized (see the initialize method options).

Defined Under Namespace

Classes: Parameter

Constant Summary collapse

CHANNEL_REGEX =

Describes all possible channel names. Omits the channel prefix, as that can vary from server to server. (See channel?)

"[^\\s\\x7,:]+"
NICK_REGEX =

The default regular expression for IRC nicknames.

"[a-zA-Z0-9\\-_\\[\\]\\{\\}\\\\|`\\^]+"
IRC_COMMANDS =

Valid IRC command names, mapped to information about their parameters.

{
  :pass => [ param('password') ],
  :nick => [ param('nickname') ],
  :user => [ param('user'), param('host'), param('server'), param('name') ],
  :oper => [ param('user'), param('password') ],
  :quit => [ param('message', :required => false, :colonize => true) ],

  :join => [ param('channels', :list => true), param('keys', :list => true) ],
  :part => [ param('channels', :list => true) ],
  :mode => [ param('channel/nick'), param('mode'), param('limit', :required => false), param('user', :required => false), param('mask', :required => false) ],
  :topic => [ param('channel'), param('topic', :required => false, :colonize => true) ],
  :names => [ param('channels', :required => false, :list => true) ],
  :list => [ param('channels', :required => false, :list => true), param('server', :required => false) ],
  :invite => [ param('nick'), param('channel') ],
  :kick => [ param('channels', :list => true), param('users', :list => true), param('comment', :required => false, :colonize => true) ],

  :version => [ param('server', :required => false) ],
  :stats => [ param('query', :required => false), param('server', :required => false) ],
  :links => [ param('server/mask', :required => false), param('server/mask', :required => false) ],
  :time => [ param('server', :required => false) ],
  :connect => [ param('target server'), param('port', :required => false), param('remote server', :required => false) ],
  :trace => [ param('server', :required => false) ],
  :admin => [ param('server', :required => false) ],
  :info => [ param('server', :required => false) ],

  :privmsg => [ param('receivers', :list => true), param('message', :colonize => true) ],
  :notice => [ param('nick'), param('message', :colonize => true) ],

  :who => [ param('name', :required => false), param('is mask', :required => false) ],
  :whois => [ param('server/nicks', :list => true), param('nicks', :list => true, :required => false) ],
  :whowas => [ param('nick'), param('history count', :required => false), param('server', :required => false) ],

  :pong => [ param('code', :required => false, :colonize => true) ]
}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from StemFacade

#change_nick, #grant_user_privilege, #grant_usermode, #join_channel, #leave_channel, #message, #privilege, #remove_user_privilege, #remove_usermode, #set_channel_property, #set_topic, #unset_channel_property, #users

Constructor Details

#initialize(server, newnick, opts) ⇒ Stem

Creates an instance that connects to a given IRC server and requests a given nick. Valid options:

port

The port that the IRC client should connect on (default 6667).

local_ip

Set this if you want to bind to an IP other than your default (for virtual hosting).

logger

Specifies a logger instance to use. If none is specified, a new LogFacade instance is created for the current season.

ssl

If true, indicates that the connection will be made over SSL.

user

The username to transmit to the IRC server (by default it’s the user’s nick).

name

The real name to transmit to the IRC server (by default it’s the user’s nick).

server_password

The server password (not the nick password), if necessary.

password

The password to send to NickServ, if your leaf’s nick is registered.

channel

The name of a channel to join.

channels

An array of channel names to join.

sever_type

The name of the server type. (See Daemon). If left blank, the default Daemon instance is used.

rejoin

If true, the stem will rejoin a channel it is kicked from.

case_sensitive_channel_names

If true, indicates to the IRC client that this IRC server uses case-sensitive channel names.

dont_ghost

If true, does not issue a /ghost command if the stem’s nick is taken. (This is only relevant if the nick is registered and password is specified.) You should use this on IRC servers that don’t use “NickServ” – otherwise someone may change their nick to NickServ and discover your password!

ghost_without_password

Set this to true if your IRC server uses hostname authentication instead of password authentication for GHOST commands.

throttle

If enabled, the stem will throttle large amounts of simultaneous messages.

throttle_rate

Sets the number of seconds that pass between consecutive PRIVMSG’s when the leaf’s output is throttled.

throttle_threshold

Sets the number of simultaneous messages that must be queued before the leaf begins throttling output.

Any channel name can be a one-item hash, in which case it is taken to be a channel name-channel password association.

Raises:

  • (ArgumentError)


290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'lib/autumn/stem.rb', line 290

def initialize(server, newnick, opts)
  raise ArgumentError, "Please specify at least one channel" unless opts[:channel] or opts[:channels]
  
  @nick = newnick
  @server = server
  @port = opts[:port]
  @port ||= 6667
  @local_ip = opts[:local_ip]
  @options = opts
  @listeners = Set.new
  @listeners << self
  @logger = @options[:logger]
  @nick_generator = Proc.new do |oldnick|
    if options[:ghost_without_password] then
      message "GHOST #{oldnick}", 'NickServ'
      nil
    elsif options[:dont_ghost] or options[:password].nil? then
      "#{oldnick}_"
    else
      message "GHOST #{oldnick} #{options[:password]}", 'NickServ'
      nil
    end
  end
  @server_type = Daemon[opts[:server_type]]
  @server_type ||= Daemon.default
  @throttle_rate = opts[:throttle_rate]
  @throttle_rate ||= 1
  @throttle_threshold = opts[:throttle_threshold]
  @throttle_threshold ||= 5
  
  @nick_regex = (opts[:nick_regex] ? opts[:nick_regex] : NICK_REGEX)
  
  @channels = Set.new
  @channels.merge opts[:channels] if opts[:channels]
  @channels << opts[:channel] if opts[:channel]
  @channels.map! do |chan|
    if chan.kind_of? Hash then
      { normalized_channel_name(chan.keys.only) => chan.values.only }
    else
      normalized_channel_name chan
    end
  end
  # Make a hash of channels to their passwords
  @channel_passwords = @channels.select { |ch| ch.kind_of? Hash }.mash { |pair| pair }
  # Strip the passwords from @channels, making it an array of channel names only
  @channels.map! { |chan| chan.kind_of?(Hash) ? chan.keys.only : chan }
  @channel_members = Hash.new
  @updating_channel_members = Hash.new # stores the NAMES list as its being built
  
  if @throttle = opts[:throttle] then
    @messages_queue = Queue.new
    @messages_thread = Thread.new do
      throttled = false
      loop do
        args = @messages_queue.pop
        throttled = true if not throttled and @messages_queue.length >= @throttle_threshold
        throttled = false if throttled and @messages_queue.empty?
        sleep @throttle_rate if throttled
        privmsg *args
      end
    end
  end
  
  @chan_mutex = Mutex.new
  @join_mutex = Mutex.new
  @socket_mutex = Mutex.new
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(meth, *args) ⇒ Object

:nodoc:



554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
# File 'lib/autumn/stem.rb', line 554

def method_missing(meth, *args) # :nodoc:
  if IRC_COMMANDS.include? meth then
    param_info = IRC_COMMANDS[meth]
    params = Array.new
    param_info.each do |param|
      raise ArgumentError, "#{param.name} is required" if args.empty? and param.required
      arg = args.shift
      next if arg.nil? or arg.empty?
      arg = (param.list and arg.kind_of? Array) ? arg.map(&:to_s).join(',') : arg.to_s
      arg = ":#{arg}" if param.colonize
      params << arg
    end
    raise ArgumentError, "Too many parameters" unless args.empty?
    transmit "#{meth.to_s.upcase} #{params.join(' ')}"
  else
    super
  end
end

Instance Attribute Details

#channel_membersObject (readonly)

A hash of channel members by channel name.



244
245
246
# File 'lib/autumn/stem.rb', line 244

def channel_members
  @channel_members
end

#channelsObject (readonly)

The channels that this stem is a member of.



231
232
233
# File 'lib/autumn/stem.rb', line 231

def channels
  @channels
end

#local_ipObject (readonly)

The local IP to bind to (virtual hosting).



226
227
228
# File 'lib/autumn/stem.rb', line 226

def local_ip
  @local_ip
end

#loggerObject (readonly)

The LogFacade instance handling this stem.



233
234
235
# File 'lib/autumn/stem.rb', line 233

def logger
  @logger
end

#nick_generatorObject (readonly)

A Proc that will be called if a nickname is in use. It should take one argument, the nickname that was unavailable, and return a new nickname to try. The default Proc appends an underscore to the nickname to produce a new one, or GHOSTs the nick if possible. This block should return nil if you do not want another NICK attempt to be made.



239
240
241
# File 'lib/autumn/stem.rb', line 239

def nick_generator
  @nick_generator
end

#optionsObject (readonly)

The global configuration options plus those for the current season and this stem.



229
230
231
# File 'lib/autumn/stem.rb', line 229

def options
  @options
end

#portObject (readonly)

The remote port that this stem is connecting to.



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

def port
  @port
end

#serverObject (readonly)

The address of the server this stem is connected to.



222
223
224
# File 'lib/autumn/stem.rb', line 222

def server
  @server
end

#server_typeObject (readonly)

The Daemon instance that describes the IRC server this client is connected to.



242
243
244
# File 'lib/autumn/stem.rb', line 242

def server_type
  @server_type
end

Class Method Details

.param(name, opts = {}) ⇒ Object

:nodoc:



181
182
183
# File 'lib/autumn/stem.rb', line 181

def self.param(name, opts={}) # :nodoc:
  Parameter.new(name, opts)
end

Instance Method Details

#add_listener(obj) ⇒ Object

Adds an object that will receive notifications of incoming IRC messages. For each IRC event that the listener is interested in, the listener should implement a method in the form irc_[event]_event, where [event] is the name of the event, as taken from the IRC_COMMANDS hash. For example, to register interest in PRIVMSG events, implement the method:

irc_privmsg_event(stem, sender, arguments)

If you wish to perform an operation each time any IRC event is received, you can implement the method:

irc_event(stem, command, sender, arguments)

The parameters for both methods are as follows:

stem

This Stem instance.

sender

A sender hash (see the Leaf docs).

arguments

A hash whose keys depend on the IRC command. Keys can be, for example, :recipient, :channel, :mode, or :message. Any can be nil.

The irc_event method also receives the command name as a symbol.

In addition to events, the Stem will also pass IRC server responses along to its listeners. Known responses (those specified by the Daemon) are translated to programmer-friendly symbols using the Daemon.event hash. The rest are left in numerical form.

If you wish to register interest in a response code, implement a method of the form irc_[response]_response, where [response] is the symbol or numerical form of the response. For instance, to register interest in channel-full errors, you’d implement:

irc_err_channelisfull_response(stem, sender, recipient, arguments, msg)

You can also register an interest in all server responses by implementing:

irc_response(stem, response, sender, recipient, arguments, msg)

This method is invoked when the server sends a response message. The parameters for both methods are:

sender

The server’s address.

recipient

The nick of the recipient (sometimes “*” if no nick has been assigned yet).

arguments

Array of response arguments, as strings.

message

An additional message attached to the end of the response.

The irc_server_response method additionally receives the response code as a symbol or numerical parameter.

Please note that there are hundreds of possible responses, and IRC servers differ in what information they send along with each response code. I recommend inspecting the output of the specific IRC server you are working with, so you know what arguments to expect.

If your listener is interested in IRC server notices, implement the method:

irc_server_notice(stem, server, sender, msg)

This method will be invoked for notices from the IRC server. Its parameters are:

server

The server’s address.

sender

The message originator (e.g., “Auth” for authentication-related messages).

msg

The notice.

If your listener is interested in IRC server errors, implement the method:

irc_server_error(stem, msg)

This method will be invoked whenever an IRC server reports an error, and is passed the error message. Server errors differ from normal server responses, which themselves can sometimes indicate errors.

Some listeners can act as listener plugins; see the broadcast method for more information.

If you’d like your listener to perform actions after it’s been added to a Stem, implement a method called added. This method will be called when the listener is added to a stem, and will be passed the Stem instance it was added to. You can use this method, for instance, to add additional methods to the stem.

Your listener can implement the stem_ready method, which will be called once the stem has started up, connected to the server, and joined all its channels. This method is passed the stem instance.



448
449
450
451
452
# File 'lib/autumn/stem.rb', line 448

def add_listener(obj)
  @listeners << obj
  obj.class.extend Anise::Annotation # give it the ability to sync
  obj.respond :added, self
end

#broadcast(meth, *args) ⇒ Object

Sends the method with the name meth (a symbol) to all listeners that respond to that method. You can optionally specify one or more arguments. This method is meant for use by listener plugins: listeners that add features to other listeners by allowing them to implement optional methods.

For example, you might have a listener plugin that adds CTCP support to stems. Such a method would parse incoming messages for CTCP commands, and then use the broadcast method to call methods named after those commands. Other listeners who want to use CTCP support can implement the methods that your listener plugin broadcasts.

Note: Each method call will be executed in its own thread, and all exceptions will be caught and reported. This method will only invoke listener methods that have not been marked as synchronized. (See “Synchronous Methods” in the class docs.)



471
472
473
474
475
476
477
478
479
480
481
482
# File 'lib/autumn/stem.rb', line 471

def broadcast(meth, *args)
  @listeners.select { |listener| not listener.class.ann(meth, :stem_sync) }.each do |listener|
    Thread.new do
      begin
        listener.respond meth, *args
      rescue Exception
        options[:logger].error $!
        message("Listener #{listener.class.to_s} raised an exception responding to #{meth}: " + $!.to_s) rescue nil # Try to report the error if possible
      end
    end
  end
end

#broadcast_sync(meth, *args) ⇒ Object

Same as the broadcast method, but only invokes listener methods that have been marked as synchronized.



487
488
489
# File 'lib/autumn/stem.rb', line 487

def broadcast_sync(meth, *args)
  @listeners.select { |listener| listener.class.ann(meth, :stem_sync) }.each { |listener| listener.respond meth, *args }
end

#channel?(str) ⇒ Boolean

Returns true if the string appears to be a channel name.

Returns:

  • (Boolean)


584
585
586
587
# File 'lib/autumn/stem.rb', line 584

def channel?(str)
  prefixes = Regexp.escape(server_type.channel_prefix.keys.join)
  str.match("[#{prefixes}]#{CHANNEL_REGEX}") != nil
end

#channel_type(channel) ⇒ Object

Given a full channel name, returns the channel type as a symbol. Values can be found in the Daemons instance. Returns :unknown for unknown channel types.



577
578
579
580
# File 'lib/autumn/stem.rb', line 577

def channel_type(channel)
  type = server_type.channel_prefix[channel[0,1]]
  type ? type : :unknown
end

#inspectObject

:nodoc:



601
602
603
# File 'lib/autumn/stem.rb', line 601

def inspect # :nodoc:
  "#<#{self.class.to_s} #{server}:#{port}>"
end

#nick?(str) ⇒ Boolean

Returns true if the string appears to be a nickname.

Returns:

  • (Boolean)


591
592
593
# File 'lib/autumn/stem.rb', line 591

def nick?(str)
  str.match(@nick_regex) != nil
end

#nicknameObject

Returns the nick this stem is using.



597
598
599
# File 'lib/autumn/stem.rb', line 597

def nickname
  @nick
end

#normalized_channel_name(channel, add_prefix = true) ⇒ Object

Normalizes a channel name by placing a “#” character before the name if no channel prefix is otherwise present. Also converts the name to lowercase if the case_sensitive_channel_names option is false. You can suppress the automatic prefixing by passing false for add_prefix.



547
548
549
550
551
552
# File 'lib/autumn/stem.rb', line 547

def normalized_channel_name(channel, add_prefix=true)
  norm_chan = channel.dup
  norm_chan.downcase! unless options[:case_sensitive_channel_names]
  norm_chan = "##{norm_chan}" unless server_type.channel_prefix?(channel[0,1]) or not add_prefix
  return norm_chan
end

#ready?Boolean

Returns true if this stem has started up completely, connected to the IRC server, and joined all its channels. A period of 10 seconds is allowed to join all channels, after which the stem will report ready even if some channels could not be joined.

Returns:

  • (Boolean)


538
539
540
# File 'lib/autumn/stem.rb', line 538

def ready?
  @ready == true
end

#startObject

Opens a connection to the IRC server and begins listening on it. This method runs until the socket is closed, and should be run in a thread. It will terminate when the connection is closed. No messages should be transmitted, nor will messages be received, until this method is called.

In the event that the nick is unavailable, the nick_generator proc will be called.



499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
# File 'lib/autumn/stem.rb', line 499

def start
  # Synchronous (mutual exclusion) message processing is handled by a
  # producer-consumer approach. The socket pushes messages onto this queue,
  # which are processed by a consumer thread one at a time.
  @messages = Queue.new
  @message_consumer = Thread.new do
    loop do
      meths = @messages.pop
      begin
        meths.each { |meth, args| broadcast_sync meth, *args }
      rescue
        options[:logger].error $!
      end
    end
  end
  
  @socket = connect
  username = @options[:user]
  username ||= @nick
  realname = @options[:name]
  realname ||= @nick

  pass @options[:server_password] if @options[:server_password]
  user username, @nick, @nick, realname
  nick @nick
  
  while line = @socket.gets
    meths = receive line # parse the line and get a list of methods to call
    @messages.push meths # push the methods on the queue; the consumer thread will execute all the synchronous methods
    # then execute all the other methods in their own thread
    meths.each { |meth, args| broadcast meth, *args }
  end
end