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 forset_callback
. The “xxx” must be replaced by the incoming event’s short type name. For example,on_welcome {|event| ...}
would be used in place ofset_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 ashearing_msg {|event| ...}
-
heard_xxx(method = nil, &block)
: Adds an after-callback filter for the given incoming event type, such asheard_msg {|event| ...}
-
saying_xxx(method = nil, &block)
: Adds a before-callback filter for the given outgoing event type, such assaying_mode {|event| ...}
-
said_xxx(method = nil, &block)
: Adds an after-callback filter for the given outgoing event type, such assaid_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 asevent.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, whileevent.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 useset_callback(:incoming_liststart) {|event| ...}
to handle the 321 numeric message, or juston_liststart {|event| ...}
. Exposesevent.target
,event.parameters
,event.message
, andevent.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. Exposesevent.channel
, the channel in question, andevent.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, andevent.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, andevent.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 inevent.message
. All arguments of the mode strings get stored as individual records in theevent.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, andevent.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, andevent.pm?
will be true. -
:incoming_ctcp
: The behavior ofevent.target
,event.channel
, andevent.pm?
will remain the same as for:incoming_msg
events.event.message
will contain the CTCP message. -
:incoming_act
: The behavior ofevent.target
,event.channel
, andevent.pm?
will remain the same as for:incoming_msg
events.event.message
will contain the ACTION message. -
:incoming_notice
: The behavior ofevent.target
,event.channel
, andevent.pm?
will remain the same as for:incoming_msg
events.event.message
will contain the NOTICE message. -
:incoming_ctcp_reply
: The behavior ofevent.target
,event.channel
, andevent.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. Ifserver
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!
- MODULE Net::YAIL::Dispatch
- CLASS Net::YAIL::BaseEvent
- CLASS Net::YAIL::CustomEvent
- CLASS Net::YAIL::Handler
- CLASS Net::YAIL::IncomingEvent
- CLASS Net::YAIL::MessageParser
- CLASS Net::YAIL::OutgoingEvent
- A
- B
- L
- M
- N
- R
- S
- Net::IRCEvents::Magic
- Net::IRCEvents::Defaults
- Net::IRCOutputAPI
- Net::IRCEvents::LegacyEvents
- Net::YAIL::Dispatch
VERSION | = | '1.6.3' |
[R] | dead_socket | |
[RW] | log | |
[R] | me | |
[R] | nicknames | |
[R] | registered | |
[R] | socket | |
[RW] | throttle_seconds |
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.
Source: show
# 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
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.
Source: show
# 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
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.
Source: show
# 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
Source: show
# 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
Source: show
# 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
Handles magic listener setup methods: on_xxx, hearing_xxx, heard_xxx, saying_xxx, and said_xxx
Source: show
# 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
Converts events that are numerics into the internal “incoming_numeric_xxx” format
Source: show
# 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
Reports may not get printed in the proper order since I scrubbed the IRCSocket report capturing, but this is way more straightforward to me.
Source: show
# 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
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.
Source: show
# 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
Source: show
# 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
Source: show
# 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
Starts listening for input and builds the perma-threads that check for input, output, and privmsg buffering.
Source: show
# 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
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.
Source: show
# 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
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!
Source: show
# 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