This library is based on the initial release of IRCSocket with a tiny bit of plagarism of Ruby-IRC.

Need an example? For a separate project you can play with that relies on Net::YAIL, check out github.com/Nerdmaster/superloud. This is based on the code in the examples directory, but is easier to clone, run, and tinker with because it’s a separate github project.

My aim here is to build something that is still fairly simple to use, but powerful enough to build a decent IRC program.

This is far from complete, but it does successfully power a relatively complicated bot, so I believe it’s solid and “good enough” for basic tasks.

Events overview

YAIL at its core is an event handler with some logic specific to IRC socket messages. BaseEvent is the parent of all event objects. An event is run through various pre-callback filters, a single callback, and post-callback filters. Up until the callback is hit, the handler “chain” can be stopped by calling the event’s .handled! method. It is generally advised against doing this, as it will stop things like post-callback stats gathering and similar plugin-friendly features, but it does make sense in certain situations (an “ignore user” module, for instance).

The life of a typical event, such as the one generated when a server message is parsed into a Net::YAIL::IncomingEvent object:

  • If the event hasn’t been handled, the event’s callback is run

  • If the event hasn’t been handled, legacy handlers are run if any are registered (TO BE REMOVED IN 2.0)

    • Legacy handlers can return true to end the chain, much like calling BaseEvent#handle! on an event object

  • If the event hasn’t been handled, all “after filters” are run (these cannot set an event as having been handled)

Callbacks and Filters

Callbacks and filters are basically handlers for a given event. The difference in a callback and filter is explained above (1 callback per event, many filters), but at their core they are just code that handles some aspect of the event.

Handler methods must receive a block of code. This can be passed in as a simple Ruby block, or manually created via Proc.new, lambda, Foo.method(:bar), etc. The method parameter of all the handler methods is optional so that, as mentioned, a block can be used instead of a Proc.

The handlers, when fired, will yield the event object containing all relevant data for the event. See the examples below for a basic idea.

To register an event’s callback, you have the following options:

  • set_callback(event_type, method = nil, &block): Sets the event type’s callback, clobbering any existing callback for that event type.

  • on_xxx(method = nil, &block): For incoming events only, this is a shortcut for set_callback. The “xxx” must be replaced by the incoming event’s short type name. For example, on_welcome {|event| ...} would be used in place of set_callback(:incoming_welcome, xxx).

To register a before- or after-callback filter, the following methods are available:

  • before_filter(event_type, method = nil, &block): Sets a before-callback filter, adding it to the current list of before-callback filters for the given event type.

  • after_filter(event_type, method = nil, &block): Sets an after-callback filter, adding it to the current list of after-callback filters for the given event type.

  • hearing_xxx(method = nil, &block): Adds a before-callback filter for the given incoming event type, such as hearing_msg {|event| ...}

  • heard_xxx(method = nil, &block): Adds an after-callback filter for the given incoming event type, such as heard_msg {|event| ...}

  • saying_xxx(method = nil, &block): Adds a before-callback filter for the given outgoing event type, such as saying_mode {|event| ...}

  • said_xxx(method = nil, &block): Adds an after-callback filter for the given outgoing event type, such as said_act {|event| ...}

Conditional Filtering

For some situations, you want your filter to only be called if a certain condition is met. Enter conditional filtering! By using this exciting feature, you can set up handlers and callbacks which only trigger when certain conditions are met. Be warned, though, this can get confusing.…

Conditions can be added to any filter method, but should *never* be used on the callback, since *there can be only one*. To add a filter, you simply supply a hash with a key of either `:if` or `:unless`, and a value which is either another hash of conditions, or a proc.

If a proc is sent, it will be a method that is called and passed the event object. If the proc returns true, an `:if` condition is met and un `:unless` condition is not met. If a condition is not met, the filter is skipped entirely.

If a hash is sent, each key is expected to be an attribute on the event object. It’s similar to a lambda where you return true if each attribute equals the value in the hash. For instance, `:if => {:message => “food”, :nick => “Simon”}` is the same as `:if => lambda {|e| e.message == “food” && e.nick == “Simon”}`.

Incoming events

All incoming events will have, at the least, the following methods:

  • raw: The raw text sent by the IRC server

  • msg: The parsed IRC message (Net::YAIL::MessageParser instance)

  • server?: Boolean flag. True if the message was generated by the server alone, false if it was generated by some kind of user action (such as a PRIVMSG sent from somebody else)

  • from: Originator of message: user’s nickname if a user message, server name otherwise

Additionally, *all messages originated by another IRC user* will have these methods:

  • fullname: The full username (“Nerdmaster!jeremy@nerdbucket.com”, for instance)

  • nick: The short nickname of a user (“Nerdmaster”, for instance) - this will be the same as event.from, but obviously only for user-initiated events.

Messages sent by the server that weren’t initiated by a user will have event.servername, which is merely the name of the server, and will be the same as event.from.

When in doubt, you can always build a filter for a particular event that spits out all its non-base methods:

yail.hearing_xxx {|e| puts e.public_methods - Net::YAIL::BaseEvent.instance_methods}

This should be a comprehensive list of all incoming events and what additional attributes the object will expose.

  • :incoming_any: A catch-all handler useful for reporting or doing top-level filtering. Before- and after-callback filters can run for all events by adding them to :incoming_any, but you cannot register a callback, as the event’s type determines its callback. :incoming_any before-callback filters can stop an event from happening on a global scale, so be careful when deciding to do anything “clever” here.

  • :incoming_error: A server error of some kind happened. event.message gives you the message sent by the server.

  • :incoming_ping: PING from server. YAIL handles this by default, so if you override the handler, you MUST send a PONG response or the server will close your connection. event.message may have a PING “message” in it. The return PONG should send out the same message as the PING received.

  • :incoming_topic_change: The topic of a channel was changed. event.channel gives you the channel in which the change occurred, while event.message gives you the message, i.e. the new topic.

  • :incoming_numeric_###: If you want, you can set up your handlers for numeric events by number, but you’ll have a much easier time looking at the eventmap.yml file included in the lib/net/yail directory. You can create an incoming handler for any event in that file. The event names will be :incoming_xxx, where “xxx” is the text of the event. For instance, you could use set_callback(:incoming_liststart) {|event| ...} to handle the 321 numeric message, or just on_liststart {|event| ...}. Exposes event.target, event.parameters, event.message, and event.numeric. You may have to experiment with different numerics to see what this data actually means for a given event.

  • :incoming_invite: INVITE message sent from a user to request your presence in another channel. Exposes event.channel, the channel in question, and event.target, which should always be your nickname.

  • :incoming_join: A user joined a channel. event.channel tells you the channel.

  • :incoming_part: A user left a channel. event.channel tells you the channel, and event.message will contain a message if the user gave one.

  • :incoming_kick: A user was kicked from a channel. event.channel tells you the channel, event.target tells you the nickname of the kicked party, and event.message will contain a message if the kicking party gave one.

  • :incoming_quit: A user quit the server. event.message will have details, if the user provided a quit message.

  • :incoming_nick: A user changed nicknames. event.message will contain the new nickname.

  • :incoming_mode: A user or server can initiate this, and this is the most screwy event in YAIL. This needs an overhaul and will hopefully change by 2.0, but for now I take the raw mode strings, such as “+bivv” and put them in event.message. All arguments of the mode strings get stored as individual records in the event.targets array. For modes like “+ob”, the first entry in targets will be the user given ops, and the second will be the ban string. I hope to overhaul this prior to 2.0, so if you rely on mode parsing, be warned.

  • :incoming_msg: A “standard” PRIVMSG event (i.e., not CTCP). event.message will contain the message, obviously. If the message is to a channel, event.channel will contain the channel name, event.target will be nil, and event.pm? will be false. If the message is sent to a user (the client running Net::YAIL), event.channel will be nil, event.target will have the user name, and event.pm? will be true.

  • :incoming_ctcp: The behavior of event.target, event.channel, and event.pm? will remain the same as for :incoming_msg events. event.message will contain the CTCP message.

  • :incoming_act: The behavior of event.target, event.channel, and event.pm? will remain the same as for :incoming_msg events. event.message will contain the ACTION message.

  • :incoming_notice: The behavior of event.target, event.channel, and event.pm? will remain the same as for :incoming_msg events. event.message will contain the NOTICE message.

  • :incoming_ctcp_reply: The behavior of event.target, event.channel, and event.pm? will remain the same as for :incoming_msg events. event.message will contain the CTCP reply message.

  • :incoming_unknown: This should NEVER happen, but just in case, it’s there. Enjoy!

Output API

All output API calls create a Net::YAIL::OutgoingEvent object and dispatch that event. After before-callback filters are processed, assuming the event wasn’t handled, the callback will send the message out to the IRC socket. If you choose to override the callback for outgoing events, rather than using filters, you will have to print the data to the socket yourself.

The parameters for the API calls will match what the outgoing event object exposes as attributes, so if there were an API call for “foo(bar, baz)”, it would generate an outgoing event of type :outgoing_foo. The data you passed in as “bar” would be available via event.bar in a handler.

There is also an :outgoing_any event type that can be used for global filtering much like the :incoming_any filtering.

The :outgoing_begin_connection event callback should never be overwritten. It exists so you can add filters before or after the initial flurry of messages to the server (USER, PASS, and NICK), but it is really an internal “helper” event. Overwriting it means you will need to write your own code to log in to the server.

This should be a comprehensive list of all outgoing methods and parameters:

  • msg(target, message): Send a PRIVMSG to the given target (channel or nickname)

  • ctcp(target, message): Sends a PRIVMSG to the given target with its message wrapped in ASCII character 1, signifying use of client-to-client protocol.

  • act(target, message): Sends a PRIVMSG to the given target with its message wrapped in the CTCP “action” syntax. A lot of IRC clients use “/me” to do this command.

  • privmsg(target, message): Sends a raw, unbuffered PRIVMSG to the given target - primarily useful for filtering, as msg, act, and ctcp all eventually call this handler.

  • notice(target, message): Sends a notice message to the given target

  • ctcpreply(target, message): Sends a notice message wrapped in ASCII 1 to signify a CTCP reply.

  • mode(target, [modes, [objects]]): Sets or requests modes for the given target (channel or user). The list of modes, if present, is applied to the target and objects if present. Modes in YAIL need some work, but here are some basic examples:

    • mode("#channel", "+b", "Nerdmaster!*@*"): bans anybody with the nickname “Nerdmaster” from subsequently joining channel.

    • mode("#channel"): Requests a list of modes on channel

    • mode("#channel", "-k"): Removes the key for channel

  • join(channel, [password]): Joins the given channel with an optional password (channel key)

  • part(channel, [message]): Leaves the given channel, with an optional message specified on part

  • quit([message]): Leaves the server with an optional message. Note that some servers will not display your quit message due to spam issues.

  • nick(nick): Changes your nickname, and updates YAIL @me variable if successful

  • user(username, hostname, servername, realname): Sets up your information upon joining a server. YAIL should generally take care of this for you in the default :outgoing_begin_connection callback.

  • pass(password): Sends a server password, not to be confused with a channel key.

  • oper(user, password): Authenticates a user as an IRC operator for the server.

  • topic(channel, [new_topic]): With no new_topic, returns the topic for a given channel. If new_topic is present, sets the topic instead.

  • names([channel]): Gets a list of all users on the network or a specific channel if specified. The channel parameter can actually contain a comma-separated list of channels if desired.

  • list([channel, [server]]: Shows all channels on the server. channel can contain a comma-separated list of channels, which will restrict the list to the given channels. If server is present, the request is forwarded to the given server.

  • invite(nick, channel): Invites a user to the given channel.

  • kick(nick, channel, [message]): Kicks the given user from the given channel with an optional message

  • <tt>whois(nick, [server]): Issues a WHOIS command for the given nickname with an optional server.

Simple Example

You should grab the source from github (github.com/Nerdmaster/ruby-irc-yail) and look at the examples directory for more interesting (but still simple) examples. But to get you started, here’s a really dumb, contrived example:

require 'rubygems'
require 'net/yail'

irc = Net::YAIL.new(
  :address    => 'irc.someplace.co.uk',
  :username   => 'Frakking Bot',
  :realname   => 'John Botfrakker',
  :nicknames  => ['bot1', 'bot2', 'bot3']
)

# Automatically join #foo when the server welcomes us
irc.on_welcome {|event| irc.join("#foo") }

# Store the last message and person who spoke - this is a filter as it doesn't need to be
# "the" definitive code run for the event
irc.hearing_msg {|event| @last_message = {:nick => event.nick, :message => event.message} }

# Loops forever until CTRL+C
irc.start_listening!
Namespace
Methods
A
B
L
M
N
R
S
Included Modules
Constants
VERSION = '1.6.3'
 
Attributes
[R] dead_socket
[RW] log
[R] me
[R] nicknames
[R] registered
[R] socket
[RW] throttle_seconds
Class Public methods
new(options = {})

Makes a new instance, obviously.

Note: I haven’t done this everywhere, but for the constructor, I felt it needed to have hash-based args. It’s just cleaner to me when you’re taking this many args.

Options:

  • :address: Name/IP of the IRC server

  • :port: Port number, defaults to 6667

  • :username: Username reported to server

  • :realname: Real name reported to server

  • :nicknames: Array of nicknames to cycle through

  • :io: TCP replacement object to use, should already be connected and ready for sending the “connect” data (:outgoing_begin_connection handler does this) If this is passed, :address and :port are ignored.

  • :silent: DEPRECATED - Sets Logger level to FATAL and silences most non-Logger messages.

  • :loud: DEPRECATED - Sets Logger level to DEBUG. Spits out too many messages for your own good, and really is only useful when debugging YAIL. Defaults to false, thankfully.

  • :throttle_seconds: Seconds between a cycle of privmsg sends. Defaults to 1. One “cycle” is defined as sending one line of output to all targets that have output buffered.

  • :server_password: Very optional. If set, this is the password sent out to the server before USER and NICK messages.

  • :log: Optional, if set uses this logger instead of the default (Ruby’s Logger). If set, :loud and :silent options are ignored.

  • :log_io: Optional, ignored if you specify your own :log - sends given object to Logger’s constructor. Must be filename or IO object.

  • :use_ssl: Defaults to false. If true, attempts to use SSL for connection.

# File lib/net/yail.rb, line 342
def initialize(options = {})
  @me                 = ''
  @nicknames          = options[:nicknames]
  @registered         = false
  @username           = options[:username]
  @realname           = options[:realname]
  @address            = options[:address]
  @io                 = options[:io]
  @port               = options[:port] || 6667
  @log_silent         = options[:silent] || false
  @log_loud           = options[:loud] || false
  @throttle_seconds   = options[:throttle_seconds] || 1
  @password           = options[:server_password]
  @ssl                = options[:use_ssl] || false

  #############################################
  # TODO: DEPRECATED!!
  #
  # TODO: Delete this!
  #############################################
  @legacy_handlers = Hash.new

  # Shared resources for threads to try and coordinate....  I know very
  # little about thread safety, so this stuff may be a terrible disaster.
  # Please send me better approaches if you are less stupid than I.
  @input_buffer = []
  @input_buffer_mutex = Mutex.new
  @privmsg_buffer = {}
  @privmsg_buffer_mutex = Mutex.new

  # Buffered output is allowed to go out right away.
  @next_message_time = Time.now

  # Setup callback/filter hashes
  @before_filters = Hash.new
  @after_filters = Hash.new
  @callback = Hash.new

  # Special handling to avoid mucking with Logger constants if we're using a different logger
  if options[:log]
    @log = options[:log]
  else
    @log = Logger.new(options[:log_io] || STDERR)
    @log.level = Logger::INFO

    if (options[:silent] || options[:loud])
      @log.warn '[DEPRECATED] - passing :silent and :loud options to constructor are deprecated as of 1.4.1'
    end

    # Convert old-school options into logger stuff
    @log.level = Logger::DEBUG if @log_loud
    @log.level = Logger::FATAL if @log_silent
  end

  # Read in map of event numbers and names.  Yes, I stole this event map
  # file from RubyIRC and made very minor changes....  They stole it from
  # somewhere else anyway, so it's okay.
  eventmap = "#{File.dirname(__FILE__)}/yail/eventmap.yml"
  @event_number_lookup = File.open(eventmap) { |file| YAML::load(file) }.invert

  if @io
    @socket = @io
  else
    prepare_tcp_socket
  end

  set_defaults
end
Instance Public methods
after_filter(event_type, method = nil, conditions = {}, &block)

Prepends the given block or method to the after_filters array for the given type. After-filters are called after the event callback has run, and cannot stop other after-filters from running. Best used for logging or statistics gathering.

# File lib/net/yail.rb, line 729
def after_filter(event_type, method = nil, conditions = {}, &block)
  filter = block_given? ? block : method
  if filter
    event_type = numeric_event_type_convert(event_type)
    @after_filters[event_type] ||= Array.new
    @after_filters[event_type].unshift(Net::YAIL::Handler.new(filter, conditions))
  end
end
before_filter(event_type, method = nil, conditions = {}, &block)

Prepends the given block or method to the before_filters array for the given type. Before-filters are called before the event callback has run, and can stop the event (and other filters) from running by calling the event’s end_chain() method. Filters shouldn’t do this very often! Before-filtering can modify output text before the event callback runs, ignore incoming events for a given user, etc.

# File lib/net/yail.rb, line 706
def before_filter(event_type, method = nil, conditions = {}, &block)
  filter = block_given? ? block : method
  if filter
    event_type = numeric_event_type_convert(event_type)
    @before_filters[event_type] ||= Array.new
    @before_filters[event_type].unshift(Net::YAIL::Handler.new(filter, conditions))
  end
end
loud()
# File lib/net/yail.rb, line 304
def loud
  @log.warn '[DEPRECATED] - Net::YAIL#loud is deprecated as of 1.4.1 - .log can be used instead'
  return @log_loud
end
loud=(val)
# File lib/net/yail.rb, line 308
def loud=(val)
  @log.warn '[DEPRECATED] - Net::YAIL#loud= is deprecated as of 1.4.1 - .log can be used instead'
  @log_loud = val
end
method_missing(name, *args, &block)

Handles magic listener setup methods: on_xxx, hearing_xxx, heard_xxx, saying_xxx, and said_xxx

# File lib/net/yail.rb, line 756
def method_missing(name, *args, &block)
  method = nil
  event_type = nil

  case name.to_s
    when %r^on_(.*)$/
      method = :set_callback
      event_type = :"incoming_#{$1}"

    when %r^hearing_(.*)$/
      method = :before_filter
      event_type = :"incoming_#{$1}"

    when %r^heard_(.*)$/
      method = :after_filter
      event_type = :"incoming_#{$1}"

    when %r^saying_(.*)$/
      method = :before_filter
      event_type = :"outgoing_#{$1}"

    when %r^said_(.*)$/
      method = :after_filter
      event_type = :"outgoing_#{$1}"
  end

  # Magic methods MUST have an arg or a block!
  filter_or_callback_method = block_given? ? block : args.shift
  conditions = args.shift || {}

  # If we didn't match a magic method signature, or we don't have the expected parameters, call
  # parent's method_missing.  Just to be safe, we also return, in case YAIL one day subclasses
  # from something that handles some method_missing stuff.
  return super if method.nil? || event_type.nil? || args.length > 0

  self.send(method, event_type, filter_or_callback_method, conditions)
end
numeric_event_type_convert(type)

Converts events that are numerics into the internal “incoming_numeric_xxx” format

# File lib/net/yail.rb, line 746
def numeric_event_type_convert(type)
  if (type.to_s =~ %r^incoming_(.*)$/)
    number = @event_number_lookup[$1].to_i
    type = :"incoming_numeric_#{number}" if number > 0
  end

  return type
end
report(*lines)

Reports may not get printed in the proper order since I scrubbed the IRCSocket report capturing, but this is way more straightforward to me.

# File lib/net/yail.rb, line 740
def report(*lines)
  @log.warn '[DEPRECATED] - Net::YAIL#report is deprecated and will be removed in 2.0 - use the logger (e.g., "@irc.log.info") instead'
  lines.each {|line| @log.info line}
end
set_callback(event_type, method = nil, conditions = {}, &block)

Sets up the callback for the given incoming event type. Note that unlike Net::YAIL 1.4.x and prior, there is no longer a concept of multiple callbacks! Use filters for that kind of functionality. Think this way: the callback is the action that takes place when an event hits. Filters are for functionality related to the event, but not the definitive callback - logging, filtering messages, stats gathering, ignoring messages from a set user, etc.

# File lib/net/yail.rb, line 719
def set_callback(event_type, method = nil, conditions = {}, &block)
  callback = block_given? ? block : method
  event_type = numeric_event_type_convert(event_type)
  @callback[event_type] = Net::YAIL::Handler.new(callback, conditions)
  @callback.delete(event_type) unless callback
end
silent()
# File lib/net/yail.rb, line 295
def silent
  @log.warn '[DEPRECATED] - Net::YAIL#silent is deprecated as of 1.4.1 - .log can be used instead'
  return @log_silent
end
silent=(val)
# File lib/net/yail.rb, line 299
def silent=(val)
  @log.warn '[DEPRECATED] - Net::YAIL#silent= is deprecated as of 1.4.1 - .log can be used instead'
  @log_silent = val
end
start_listening()

Starts listening for input and builds the perma-threads that check for input, output, and privmsg buffering.

# File lib/net/yail.rb, line 413
def start_listening
  # We don't want to spawn an extra listener
  return if Thread === @ioloop_thread

  # Don't listen if socket is dead
  return if @dead_socket

  # Exit a bit more gracefully than just crashing out - allow any :outgoing_quit filters to run,
  # and even give the server a second to clean up before we fry the connection
  #
  # TODO: This REALLY doesn't belong here!  This is saying everybody who uses the lib wants
  #       CTRL+C to end the app at the YAIL level.  Not necessarily true outside bot-land.
  quithandler = lambda { quit('Terminated by user'); sleep 1; stop_listening; exit }
  trap("INT", quithandler)
  trap("TERM", quithandler)

  # Begin the listening thread
  @ioloop_thread = Thread.new {io_loop}
  @input_processor = Thread.new {process_input_loop}
  @privmsg_processor = Thread.new {process_privmsg_loop}

  # Let's begin the cycle by telling the server who we are.  This should start a TERRIBLE CHAIN OF EVENTS!!!
  dispatch OutgoingEvent.new(:type => :begin_connection, :username => @username, :address => @address, :realname => @realname)
end
start_listening!()

This starts the connection, threading, etc. as #start_listening, but forces the user into and endless loop. Great for a simplistic bot, but probably not universally desired.

# File lib/net/yail.rb, line 440
def start_listening!
  start_listening
  while !@dead_socket
    # This is more for CPU savings than actually needing a delay - CPU spikes if we never sleep
    sleep 0.05
  end
end
stop_listening()

Kills and clears all threads. See note above about my lack of knowledge regarding threads. Please help me if you know how to make this system better. DEAR LORD HELP ME IF YOU CAN!

# File lib/net/yail.rb, line 451
def stop_listening
  return unless Thread === @ioloop_thread

  # Do thread-ending in a new thread or else we're liable to kill the
  # thread that's called this method
  Thread.new do
    # Kill all threads if they're really threads
    [@ioloop_thread, @input_processor, @privmsg_processor].each {|thread| thread.terminate if Thread === thread}

    @socket.close
    @socket = nil
    @dead_socket = true

    @ioloop_thread = nil
    @input_processor = nil
    @privmsg_processor = nil
  end
end