Class: Autumn::Stem
- Inherits:
-
Object
- Object
- Autumn::Stem
- 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
-
#channel_members ⇒ Object
readonly
A hash of channel members by channel name.
-
#channels ⇒ Object
readonly
The channels that this stem is a member of.
-
#local_ip ⇒ Object
readonly
The local IP to bind to (virtual hosting).
-
#logger ⇒ Object
readonly
The LogFacade instance handling this stem.
-
#nick_generator ⇒ Object
readonly
A Proc that will be called if a nickname is in use.
-
#options ⇒ Object
readonly
The global configuration options plus those for the current season and this stem.
-
#port ⇒ Object
readonly
The remote port that this stem is connecting to.
-
#server ⇒ Object
readonly
The address of the server this stem is connected to.
-
#server_type ⇒ Object
readonly
The Daemon instance that describes the IRC server this client is connected to.
Class Method Summary collapse
-
.param(name, opts = {}) ⇒ Object
:nodoc:.
Instance Method Summary collapse
-
#add_listener(obj) ⇒ Object
Adds an object that will receive notifications of incoming IRC messages.
-
#broadcast(meth, *args) ⇒ Object
Sends the method with the name
meth
(a symbol) to all listeners that respond to that method. -
#broadcast_sync(meth, *args) ⇒ Object
Same as the broadcast method, but only invokes listener methods that have been marked as synchronized.
-
#channel?(str) ⇒ Boolean
Returns true if the string appears to be a channel name.
-
#channel_type(channel) ⇒ Object
Given a full channel name, returns the channel type as a symbol.
-
#initialize(server, newnick, opts) ⇒ Stem
constructor
Creates an instance that connects to a given IRC server and requests a given nick.
-
#inspect ⇒ Object
:nodoc:.
-
#method_missing(meth, *args) ⇒ Object
:nodoc:.
-
#nick?(str) ⇒ Boolean
Returns true if the string appears to be a nickname.
-
#nickname ⇒ Object
Returns the nick this stem is using.
-
#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.
-
#ready? ⇒ Boolean
Returns true if this stem has started up completely, connected to the IRC server, and joined all its channels.
-
#start ⇒ Object
Opens a connection to the IRC server and begins listening on it.
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.
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 [:ghost_without_password] then "GHOST #{oldnick}", 'NickServ' nil elsif [:dont_ghost] or [:password].nil? then "#{oldnick}_" else "GHOST #{oldnick} #{[: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_members ⇒ Object (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 |
#channels ⇒ Object (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_ip ⇒ Object (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 |
#logger ⇒ Object (readonly)
The LogFacade instance handling this stem.
233 234 235 |
# File 'lib/autumn/stem.rb', line 233 def logger @logger end |
#nick_generator ⇒ Object (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 |
#options ⇒ Object (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 end |
#port ⇒ Object (readonly)
The remote port that this stem is connecting to.
224 225 226 |
# File 'lib/autumn/stem.rb', line 224 def port @port end |
#server ⇒ Object (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_type ⇒ Object (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
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 [:logger].error $! ("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.
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 |
#inspect ⇒ Object
: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.
591 592 593 |
# File 'lib/autumn/stem.rb', line 591 def nick?(str) str.match(@nick_regex) != nil end |
#nickname ⇒ Object
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 [: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.
538 539 540 |
# File 'lib/autumn/stem.rb', line 538 def ready? @ready == true end |
#start ⇒ Object
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 [: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 |