3 # * do we want to handle a Channel list for each User telling which
\r
4 # Channels is the User on (of those the client is on too)?
\r
5 # We may want this so that when a User leaves all Channels and he hasn't
\r
6 # sent us privmsgs, we know we can remove him from the Server @users list
\r
7 # * Maybe ChannelList and UserList should be HashesOf instead of ArrayOf?
\r
8 # See items marked as TODO Ho.
\r
9 # The framework to do this is now in place, thanks to the new [] method
\r
10 # for NetmaskList, which allows retrieval by Netmask or String
\r
12 # :title: IRC module
\r
16 # This module defines the fundamental building blocks for IRC
\r
18 # Author:: Giuseppe Bilotta (giuseppe.bilotta@gmail.com)
\r
19 # Copyright:: Copyright (c) 2006 Giuseppe Bilotta
\r
26 # We extend the Object class with a method that
\r
27 # checks if the receiver is nil or empty
\r
29 return true unless self
\r
30 return true if self.respond_to? :empty and self.empty?
\r
34 # We alias the to_s method to __to_s__ to make
\r
35 # it accessible in all classes
\r
36 alias :__to_s__ :to_s
\r
39 # The Irc module is used to keep all IRC-related classes
\r
40 # in the same namespace
\r
45 # Due to its Scandinavian origins, IRC has strange case mappings, which
\r
46 # consider the characters <tt>{}|^</tt> as the uppercase
\r
47 # equivalents of # <tt>[]\~</tt>.
\r
49 # This is however not the same on all IRC servers: some use standard ASCII
\r
50 # casemapping, other do not consider <tt>^</tt> as the uppercase of
\r
56 # Create a new casemap with name _name_, uppercase characters _upper_ and
\r
57 # lowercase characters _lower_
\r
59 def initialize(name, upper, lower)
\r
61 raise "Casemap #{name.inspect} already exists!" if @@casemaps.has_key?(@key)
\r
62 @@casemaps[@key] = {
\r
69 # Returns the Casemap with the given name
\r
71 def Casemap.get(name)
\r
72 @@casemaps[name.to_sym][:casemap]
\r
75 # Retrieve the 'uppercase characters' of this Casemap
\r
78 @@casemaps[@key][:upper]
\r
81 # Retrieve the 'lowercase characters' of this Casemap
\r
84 @@casemaps[@key][:lower]
\r
87 # Return a Casemap based on the receiver
\r
93 # A Casemap is represented by its lower/upper mappings
\r
96 self.__to_s__[0..-2] + " #{upper.inspect} ~(#{self})~ #{lower.inspect}>"
\r
99 # As a String we return our name
\r
105 # Two Casemaps are equal if they have the same upper and lower ranges
\r
108 other = arg.to_irc_casemap
\r
109 return self.upper == other.upper && self.lower == other.lower
\r
112 # Raise an error if _arg_ and self are not the same Casemap
\r
115 other = arg.to_irc_casemap
\r
116 raise "Casemap mismatch (#{self.inspect} != #{other.inspect})" unless self == other
\r
122 # The rfc1459 casemap
\r
124 class RfcCasemap < Casemap
\r
128 super('rfc1459', "\x41-\x5e", "\x61-\x7e")
\r
132 RfcCasemap.instance
\r
134 # The strict-rfc1459 Casemap
\r
136 class StrictRfcCasemap < Casemap
\r
140 super('strict-rfc1459', "\x41-\x5d", "\x61-\x7d")
\r
144 StrictRfcCasemap.instance
\r
146 # The ascii Casemap
\r
148 class AsciiCasemap < Casemap
\r
152 super('ascii', "\x41-\x5a", "\x61-\x7a")
\r
156 AsciiCasemap.instance
\r
159 # This module is included by all classes that are either bound to a server
\r
160 # or should have a casemap.
\r
162 module ServerOrCasemap
\r
164 attr_reader :server
\r
166 # This method initializes the instance variables @server and @casemap
\r
167 # according to the values of the hash keys :server and :casemap in _opts_
\r
169 def init_server_or_casemap(opts={})
\r
170 @server = opts.fetch(:server, nil)
\r
171 raise TypeError, "#{@server} is not a valid Irc::Server" if @server and not @server.kind_of?(Server)
\r
173 @casemap = opts.fetch(:casemap, nil)
\r
176 @server.casemap.must_be(@casemap)
\r
180 @casemap = (@casemap || 'rfc1459').to_irc_casemap
\r
184 # This is an auxiliary method: it returns true if the receiver fits the
\r
185 # server and casemap specified in _opts_, false otherwise.
\r
187 def fits_with_server_and_casemap?(opts={})
\r
188 srv = opts.fetch(:server, nil)
\r
189 cmap = opts.fetch(:casemap, nil)
\r
190 cmap = cmap.to_irc_casemap unless cmap.nil?
\r
193 return true if cmap.nil? or cmap == casemap
\r
195 return true if srv == @server and (cmap.nil? or cmap == casemap)
\r
200 # Returns the casemap of the receiver, by looking at the bound
\r
201 # @server (if possible) or at the @casemap otherwise
\r
204 return @server.casemap if defined?(@server) and @server
\r
208 # Returns a hash with the current @server and @casemap as values of
\r
209 # :server and :casemap
\r
211 def server_and_casemap
\r
213 h[:server] = @server if defined?(@server) and @server
\r
214 h[:casemap] = @casemap if defined?(@casemap) and @casemap
\r
218 # We allow up/downcasing with a different casemap
\r
220 def irc_downcase(cmap=casemap)
\r
221 self.to_s.irc_downcase(cmap)
\r
224 # Up/downcasing something that includes this module returns its
\r
225 # Up/downcased to_s form
\r
231 # We allow up/downcasing with a different casemap
\r
233 def irc_upcase(cmap=casemap)
\r
234 self.to_s.irc_upcase(cmap)
\r
237 # Up/downcasing something that includes this module returns its
\r
238 # Up/downcased to_s form
\r
249 # We start by extending the String class
\r
250 # with some IRC-specific methods
\r
254 # This method returns the Irc::Casemap whose name is the receiver
\r
257 Irc::Casemap.get(self) rescue raise TypeError, "Unkown Irc::Casemap #{self.inspect}"
\r
260 # This method returns a string which is the downcased version of the
\r
261 # receiver, according to the given _casemap_
\r
264 def irc_downcase(casemap='rfc1459')
\r
265 cmap = casemap.to_irc_casemap
\r
266 self.tr(cmap.upper, cmap.lower)
\r
269 # This is the same as the above, except that the string is altered in place
\r
271 # See also the discussion about irc_downcase
\r
273 def irc_downcase!(casemap='rfc1459')
\r
274 cmap = casemap.to_irc_casemap
\r
275 self.tr!(cmap.upper, cmap.lower)
\r
278 # Upcasing functions are provided too
\r
280 # See also the discussion about irc_downcase
\r
282 def irc_upcase(casemap='rfc1459')
\r
283 cmap = casemap.to_irc_casemap
\r
284 self.tr(cmap.lower, cmap.upper)
\r
287 # In-place upcasing
\r
289 # See also the discussion about irc_downcase
\r
291 def irc_upcase!(casemap='rfc1459')
\r
292 cmap = casemap.to_irc_casemap
\r
293 self.tr!(cmap.lower, cmap.upper)
\r
296 # This method checks if the receiver contains IRC glob characters
\r
298 # IRC has a very primitive concept of globs: a <tt>*</tt> stands for "any
\r
299 # number of arbitrary characters", a <tt>?</tt> stands for "one and exactly
\r
300 # one arbitrary character". These characters can be escaped by prefixing them
\r
301 # with a slash (<tt>\\</tt>).
\r
303 # A known limitation of this glob syntax is that there is no way to escape
\r
304 # the escape character itself, so it's not possible to build a glob pattern
\r
305 # where the escape character precedes a glob.
\r
308 self =~ /^[*?]|[^\\][*?]/
\r
311 # This method is used to convert the receiver into a Regular Expression
\r
312 # that matches according to the IRC glob syntax
\r
315 regmask = Regexp.escape(self)
\r
316 regmask.gsub!(/(\\\\)?\\[*?]/) { |m|
\r
325 raise "Unexpected match #{m} when converting #{self}"
\r
328 Regexp.new("^#{regmask}$")
\r
334 # ArrayOf is a subclass of Array whose elements are supposed to be all
\r
335 # of the same class. This is not intended to be used directly, but rather
\r
336 # to be subclassed as needed (see for example Irc::UserList and Irc::NetmaskList)
\r
338 # Presently, only very few selected methods from Array are overloaded to check
\r
339 # if the new elements are the correct class. An orthodox? method is provided
\r
340 # to check the entire ArrayOf against the appropriate class.
\r
342 class ArrayOf < Array
\r
344 attr_reader :element_class
\r
346 # Create a new ArrayOf whose elements are supposed to be all of type _kl_,
\r
347 # optionally filling it with the elements from the Array argument.
\r
349 def initialize(kl, ar=[])
\r
350 raise TypeError, "#{kl.inspect} must be a class name" unless kl.kind_of?(Class)
\r
352 @element_class = kl
\r
357 raise TypeError, "#{self.class} can only be initialized from an Array"
\r
362 self.__to_s__[0..-2].sub(/:[^:]+$/,"[#{@element_class}]\\0") + " #{super}>"
\r
365 # Private method to check the validity of the elements passed to it
\r
366 # and optionally raise an error
\r
368 # TODO should it accept nils as valid?
\r
370 def internal_will_accept?(raising, *els)
\r
372 unless el.kind_of?(@element_class)
\r
373 raise TypeError, "#{el.inspect} is not of class #{@element_class}" if raising
\r
379 private :internal_will_accept?
\r
381 # This method checks if the passed arguments are acceptable for our ArrayOf
\r
383 def will_accept?(*els)
\r
384 internal_will_accept?(false, *els)
\r
387 # This method checks that all elements are of the appropriate class
\r
390 will_accept?(*self)
\r
393 # This method is similar to the above, except that it raises an exception
\r
394 # if the receiver is not valid
\r
397 raise TypeError unless valid?
\r
400 # Overloaded from Array#<<, checks for appropriate class of argument
\r
403 super(el) if internal_will_accept?(true, el)
\r
406 # Overloaded from Array#&, checks for appropriate class of argument elements
\r
410 ArrayOf.new(@element_class, r) if internal_will_accept?(true, *r)
\r
413 # Overloaded from Array#+, checks for appropriate class of argument elements
\r
416 ArrayOf.new(@element_class, super(ar)) if internal_will_accept?(true, *ar)
\r
419 # Overloaded from Array#-, so that an ArrayOf is returned. There is no need
\r
420 # to check the validity of the elements in the argument
\r
423 ArrayOf.new(@element_class, super(ar)) # if internal_will_accept?(true, *ar)
\r
426 # Overloaded from Array#|, checks for appropriate class of argument elements
\r
429 ArrayOf.new(@element_class, super(ar)) if internal_will_accept?(true, *ar)
\r
432 # Overloaded from Array#concat, checks for appropriate class of argument
\r
436 super(ar) if internal_will_accept?(true, *ar)
\r
439 # Overloaded from Array#insert, checks for appropriate class of argument
\r
442 def insert(idx, *ar)
\r
443 super(idx, *ar) if internal_will_accept?(true, *ar)
\r
446 # Overloaded from Array#replace, checks for appropriate class of argument
\r
450 super(ar) if (ar.kind_of?(ArrayOf) && ar.element_class <= @element_class) or internal_will_accept?(true, *ar)
\r
453 # Overloaded from Array#push, checks for appropriate class of argument
\r
457 super(*ar) if internal_will_accept?(true, *ar)
\r
460 # Overloaded from Array#unshift, checks for appropriate class of argument(s)
\r
464 super(el) if internal_will_accept?(true, *els)
\r
468 # We introduce the 'downcase' method, which maps downcase() to all the Array
\r
469 # elements, properly failing when the elements don't have a downcase method
\r
472 self.map { |el| el.downcase }
\r
475 # Modifying methods which we don't handle yet are made private
\r
477 private :[]=, :collect!, :map!, :fill, :flatten!
\r
482 # We extend the Regexp class with an Irc module which will contain some
\r
483 # Irc-specific regexps
\r
487 # We start with some general-purpose ones which will be used in the
\r
488 # Irc module too, but are useful regardless
\r
490 HEX_DIGIT = /[0-9A-Fa-f]/
\r
491 HEX_DIGITS = /#{HEX_DIGIT}+/
\r
492 HEX_OCTET = /#{HEX_DIGIT}#{HEX_DIGIT}?/
\r
493 DEC_OCTET = /[01]?\d?\d|2[0-4]\d|25[0-5]/
\r
494 DEC_IP_ADDR = /#{DEC_OCTET}.#{DEC_OCTET}.#{DEC_OCTET}.#{DEC_OCTET}/
\r
495 HEX_IP_ADDR = /#{HEX_OCTET}.#{HEX_OCTET}.#{HEX_OCTET}.#{HEX_OCTET}/
\r
496 IP_ADDR = /#{DEC_IP_ADDR}|#{HEX_IP_ADDR}/
\r
498 # IPv6, from Resolv::IPv6, without the \A..\z anchors
\r
499 HEX_16BIT = /#{HEX_DIGIT}{1,4}/
\r
500 IP6_8Hex = /(?:#{HEX_16BIT}:){7}#{HEX_16BIT}/
\r
501 IP6_CompressedHex = /((?:#{HEX_16BIT}(?::#{HEX_16BIT})*)?)::((?:#{HEX_16BIT}(?::#{HEX_16BIT})*)?)/
\r
502 IP6_6Hex4Dec = /((?:#{HEX_16BIT}:){6,6})#{DEC_IP_ADDR}/
\r
503 IP6_CompressedHex4Dec = /((?:#{HEX_16BIT}(?::#{HEX_16BIT})*)?)::((?:#{HEX_16BIT}:)*)#{DEC_IP_ADDR}/
\r
504 IP6_ADDR = /(?:#{IP6_8Hex})|(?:#{IP6_CompressedHex})|(?:#{IP6_6Hex4Dec})|(?:#{IP6_CompressedHex4Dec})/
\r
506 # We start with some IRC related regular expressions, used to match
\r
507 # Irc::User nicks and users and Irc::Channel names
\r
509 # For each of them we define two versions of the regular expression:
\r
510 # * a generic one, which should match for any server but may turn out to
\r
511 # match more than a specific server would accept
\r
512 # * an RFC-compliant matcher
\r
516 # Channel-name-matching regexps
\r
517 CHAN_FIRST = /[#&+]/
\r
518 CHAN_SAFE = /![A-Z0-9]{5}/
\r
519 CHAN_ANY = /[^\x00\x07\x0A\x0D ,:]/
\r
520 GEN_CHAN = /(?:#{CHAN_FIRST}|#{CHAN_SAFE})#{CHAN_ANY}+/
\r
521 RFC_CHAN = /#{CHAN_FIRST}#{CHAN_ANY}{1,49}|#{CHAN_SAFE}#{CHAN_ANY}{1,44}/
\r
523 # Nick-matching regexps
\r
524 SPECIAL_CHAR = /[\x5b-\x60\x7b-\x7d]/
\r
525 NICK_FIRST = /#{SPECIAL_CHAR}|[[:alpha:]]/
\r
526 NICK_ANY = /#{SPECIAL_CHAR}|[[:alnum:]]|-/
\r
527 GEN_NICK = /#{NICK_FIRST}#{NICK_ANY}+/
\r
528 RFC_NICK = /#{NICK_FIRST}#{NICK_ANY}{0,8}/
\r
530 USER_CHAR = /[^\x00\x0a\x0d @]/
\r
531 GEN_USER = /#{USER_CHAR}+/
\r
533 # Host-matching regexps
\r
534 HOSTNAME_COMPONENT = /[[:alnum:]](?:[[:alnum:]]|-)*[[:alnum:]]*/
\r
535 HOSTNAME = /#{HOSTNAME_COMPONENT}(?:\.#{HOSTNAME_COMPONENT})*/
\r
536 HOSTADDR = /#{IP_ADDR}|#{IP6_ADDR}/
\r
538 GEN_HOST = /#{HOSTNAME}|#{HOSTADDR}/
\r
540 # # FreeNode network replaces the host of affiliated users with
\r
541 # # 'virtual hosts'
\r
542 # # FIXME we need the true syntax to match it properly ...
\r
543 # PDPC_HOST_PART = /[0-9A-Za-z.-]+/
\r
544 # PDPC_HOST = /#{PDPC_HOST_PART}(?:\/#{PDPC_HOST_PART})+/
\r
546 # # NOTE: the final optional and non-greedy dot is needed because some
\r
547 # # servers (e.g. FreeNode) send the hostname of the services as "services."
\r
548 # # which is not RFC compliant, but sadly done.
\r
549 # GEN_HOST_EXT = /#{PDPC_HOST}|#{GEN_HOST}\.??/
\r
551 # Sadly, different networks have different, RFC-breaking ways of cloaking
\r
552 # the actualy host address: see above for an example to handle FreeNode.
\r
553 # Another example would be Azzurra, wich also inserts a "=" in the
\r
554 # cloacked host. So let's just not care about this and go with the simplest
\r
556 GEN_HOST_EXT = /\S+/
\r
558 # User-matching Regexp
\r
559 GEN_USER_ID = /(#{GEN_NICK})(?:(?:!(#{GEN_USER}))?@(#{GEN_HOST_EXT}))?/
\r
561 # Things such has the BIP proxy send invalid nicks in a complete netmask,
\r
562 # so we want to match this, rather: this matches either a compliant nick
\r
563 # or a a string with a very generic nick, a very generic hostname after an
\r
564 # @ sign, and an optional user after a !
\r
565 BANG_AT = /#{GEN_NICK}|\S+?(?:!\S+?)?@\S+?/
\r
567 # # For Netmask, we want to allow wildcards * and ? in the nick
\r
568 # # (they are already allowed in the user and host part
\r
569 # GEN_NICK_MASK = /(?:#{NICK_FIRST}|[?*])?(?:#{NICK_ANY}|[?*])+/
\r
571 # # Netmask-matching Regexp
\r
572 # GEN_MASK = /(#{GEN_NICK_MASK})(?:(?:!(#{GEN_USER}))?@(#{GEN_HOST_EXT}))?/
\r
582 # A Netmask identifies each user by collecting its nick, username and
\r
583 # hostname in the form <tt>nick!user@host</tt>
\r
585 # Netmasks can also contain glob patterns in any of their components; in
\r
586 # this form they are used to refer to more than a user or to a user
\r
587 # appearing under different forms.
\r
590 # * <tt>*!*@*</tt> refers to everybody
\r
591 # * <tt>*!someuser@somehost</tt> refers to user +someuser+ on host +somehost+
\r
592 # regardless of the nick used.
\r
596 # Netmasks have an associated casemap unless they are bound to a server
\r
598 include ServerOrCasemap
\r
600 attr_reader :nick, :user, :host
\r
603 # Create a new Netmask from string _str_, which must be in the form
\r
604 # _nick_!_user_@_host_
\r
606 # It is possible to specify a server or a casemap in the optional Hash:
\r
607 # these are used to associate the Netmask with the given server and to set
\r
608 # its casemap: if a server is specified and a casemap is not, the server's
\r
609 # casemap is used. If both a server and a casemap are specified, the
\r
610 # casemap must match the server's casemap or an exception will be raised.
\r
612 # Empty +nick+, +user+ or +host+ are converted to the generic glob pattern
\r
614 def initialize(str="", opts={})
\r
615 # First of all, check for server/casemap option
\r
617 init_server_or_casemap(opts)
\r
619 # Now we can see if the given string _str_ is an actual Netmask
\r
620 if str.respond_to?(:to_str)
\r
622 # We match a pretty generic string, to work around non-compliant
\r
624 when /^(?:(\S+?)(?:(?:!(\S+?))?@(\S+))?)?$/
\r
625 # We do assignment using our internal methods
\r
630 raise ArgumentError, "#{str.to_str.inspect} does not represent a valid #{self.class}"
\r
633 raise TypeError, "#{str} cannot be converted to a #{self.class}"
\r
637 # A Netmask is easily converted to a String for the usual representation.
\r
638 # We skip the user or host parts if they are "*", unless we've been asked
\r
639 # for the full form
\r
643 ret << "!" << user unless user == "*"
\r
644 ret << "@" << host unless host == "*"
\r
649 "#{nick}!#{user}@#{host}"
\r
652 alias :to_str :fullform
\r
654 # This method downcases the fullform of the netmask. While this may not be
\r
655 # significantly different from the #downcase() method provided by the
\r
656 # ServerOrCasemap mixin, it's significantly different for Netmask
\r
657 # subclasses such as User whose simple downcasing uses the nick only.
\r
659 def full_irc_downcase(cmap=casemap)
\r
660 self.fullform.irc_downcase(cmap)
\r
663 # full_downcase() will return the fullform downcased according to the
\r
664 # User's own casemap
\r
667 self.full_irc_downcase
\r
670 # Converts the receiver into a Netmask with the given (optional)
\r
671 # server/casemap association. We return self unless a conversion
\r
672 # is needed (different casemap/server)
\r
674 # Subclasses of Netmask will return a new Netmask, using full_downcase
\r
676 def to_irc_netmask(opts={})
\r
677 if self.class == Netmask
\r
678 return self if fits_with_server_and_casemap?(opts)
\r
680 return self.full_downcase.to_irc_netmask(server_and_casemap.merge(opts))
\r
683 # Converts the receiver into a User with the given (optional)
\r
684 # server/casemap association. We return self unless a conversion
\r
685 # is needed (different casemap/server)
\r
687 def to_irc_user(opts={})
\r
688 self.fullform.to_irc_user(server_and_casemap.merge(opts))
\r
691 # Inspection of a Netmask reveals the server it's bound to (if there is
\r
692 # one), its casemap and the nick, user and host part
\r
695 str = self.__to_s__[0..-2]
\r
696 str << " @server=#{@server}" if defined?(@server) and @server
\r
697 str << " @nick=#{@nick.inspect} @user=#{@user.inspect}"
\r
698 str << " @host=#{@host.inspect} casemap=#{casemap.inspect}"
\r
702 # Equality: two Netmasks are equal if they downcase to the same thing
\r
704 # TODO we may want it to try other.to_irc_netmask
\r
707 return false unless other.kind_of?(self.class)
\r
708 self.downcase == other.downcase
\r
711 # This method changes the nick of the Netmask, defaulting to the generic
\r
712 # glob pattern if the result is the null string.
\r
715 @nick = newnick.to_s
\r
716 @nick = "*" if @nick.empty?
\r
719 # This method changes the user of the Netmask, defaulting to the generic
\r
720 # glob pattern if the result is the null string.
\r
723 @user = newuser.to_s
\r
724 @user = "*" if @user.empty?
\r
726 alias :ident= :user=
\r
728 # This method changes the hostname of the Netmask, defaulting to the generic
\r
729 # glob pattern if the result is the null string.
\r
732 @host = newhost.to_s
\r
733 @host = "*" if @host.empty?
\r
736 # We can replace everything at once with data from another Netmask
\r
744 @server = other.server
\r
745 @casemap = other.casemap unless @server
\r
747 replace(other.to_irc_netmask(server_and_casemap))
\r
751 # This method checks if a Netmask is definite or not, by seeing if
\r
752 # any of its components are defined by globs
\r
755 return @nick.has_irc_glob? || @user.has_irc_glob? || @host.has_irc_glob?
\r
760 unless u.has_irc_glob?
\r
761 u.sub!(/^[in]=/, '=') or u.sub!(/^\W(\w+)/, '\1')
\r
766 unless h.has_irc_glob?
\r
768 h.sub!(/x-\w+$/, 'x-*')
\r
770 h.match(/^[^\.]+\.[^\.]+$/) or
\r
771 h.sub!(/^(\d+\.\d+\.\d+\.)\d+$/, '\1*') or
\r
772 h.sub!(/^[^\.]+\./, '*.')
\r
775 return Netmask.new("*!#{u}@#{h}", server_and_casemap)
\r
778 # This method is used to match the current Netmask against another one
\r
780 # The method returns true if each component of the receiver matches the
\r
781 # corresponding component of the argument. By _matching_ here we mean
\r
782 # that any netmask described by the receiver is also described by the
\r
785 # In this sense, matching is rather simple to define in the case when the
\r
786 # receiver has no globs: it is just necessary to check if the argument
\r
787 # describes the receiver, which can be done by matching it against the
\r
788 # argument converted into an IRC Regexp (see String#to_irc_regexp).
\r
790 # The situation is also easy when the receiver has globs and the argument
\r
791 # doesn't, since in this case the result is false.
\r
793 # The more complex case in which both the receiver and the argument have
\r
794 # globs is not handled yet.
\r
797 cmp = arg.to_irc_netmask(:casemap => casemap)
\r
798 debug "Matching #{self.fullform} against #{arg.inspect} (#{cmp.fullform})"
\r
799 [:nick, :user, :host].each { |component|
\r
800 us = self.send(component).irc_downcase(casemap)
\r
801 them = cmp.send(component).irc_downcase(casemap)
\r
802 if us.has_irc_glob? && them.has_irc_glob?
\r
804 warn NotImplementedError
\r
807 return false if us.has_irc_glob? && !them.has_irc_glob?
\r
808 return false unless us =~ them.to_irc_regexp
\r
813 # Case equality. Checks if arg matches self
\r
816 arg.to_irc_netmask(:casemap => casemap).matches?(self)
\r
819 # Sorting is done via the fullform
\r
824 self.fullform.irc_downcase(casemap) <=> arg.fullform.irc_downcase(casemap)
\r
826 self.downcase <=> arg.downcase
\r
833 # A NetmaskList is an ArrayOf <code>Netmask</code>s
\r
835 class NetmaskList < ArrayOf
\r
837 # Create a new NetmaskList, optionally filling it with the elements from
\r
838 # the Array argument fed to it.
\r
840 def initialize(ar=[])
\r
844 # We enhance the [] method by allowing it to pick an element that matches
\r
845 # a given Netmask, a String or a Regexp
\r
846 # TODO take into consideration the opportunity to use select() instead of
\r
847 # find(), and/or a way to let the user choose which one to take (second
\r
851 if args.length == 1
\r
855 mask.matches?(args[0])
\r
859 mask.matches?(args[0].to_irc_netmask(:casemap => mask.casemap))
\r
863 mask.fullform =~ args[0]
\r
880 # We keep extending String, this time adding a method that converts a
\r
881 # String into an Irc::Netmask object
\r
883 def to_irc_netmask(opts={})
\r
884 Irc::Netmask.new(self, opts)
\r
893 # An IRC User is identified by his/her Netmask (which must not have globs).
\r
894 # In fact, User is just a subclass of Netmask.
\r
896 # Ideally, the user and host information of an IRC User should never
\r
897 # change, and it shouldn't contain glob patterns. However, IRC is somewhat
\r
898 # idiosincratic and it may be possible to know the nick of a User much before
\r
899 # its user and host are known. Moreover, some networks (namely Freenode) may
\r
900 # change the hostname of a User when (s)he identifies with Nickserv.
\r
902 # As a consequence, we must allow changes to a User host and user attributes.
\r
903 # We impose a restriction, though: they may not contain glob patterns, except
\r
904 # for the special case of an unknown user/host which is represented by a *.
\r
906 # It is possible to create a totally unknown User (e.g. for initializations)
\r
907 # by setting the nick to * too.
\r
910 # * see if it's worth to add the other USER data
\r
911 # * see if it's worth to add NICKSERV status
\r
913 class User < Netmask
\r
916 attr_accessor :real_name
\r
918 # Create a new IRC User from a given Netmask (or anything that can be converted
\r
919 # into a Netmask) provided that the given Netmask does not have globs.
\r
921 def initialize(str="", opts={})
\r
923 raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if nick.has_irc_glob? && nick != "*"
\r
924 raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if user.has_irc_glob? && user != "*"
\r
925 raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if host.has_irc_glob? && host != "*"
\r
927 @real_name = String.new
\r
930 # The nick of a User may be changed freely, but it must not contain glob patterns.
\r
933 raise "Can't change the nick to #{newnick}" if defined?(@nick) and newnick.has_irc_glob?
\r
937 # We have to allow changing the user of an Irc User due to some networks
\r
938 # (e.g. Freenode) changing hostmasks on the fly. We still check if the new
\r
939 # user data has glob patterns though.
\r
942 raise "Can't change the username to #{newuser}" if defined?(@user) and newuser.has_irc_glob?
\r
946 # We have to allow changing the host of an Irc User due to some networks
\r
947 # (e.g. Freenode) changing hostmasks on the fly. We still check if the new
\r
948 # host data has glob patterns though.
\r
951 raise "Can't change the hostname to #{newhost}" if defined?(@host) and newhost.has_irc_glob?
\r
955 # Checks if a User is well-known or not by looking at the hostname and user
\r
958 return nick != "*" && user != "*" && host != "*"
\r
961 # Is the user away?
\r
967 # Set the away status of the user. Use away=(nil) or away=(false)
\r
978 # Since to_irc_user runs the same checks on server and channel as
\r
979 # to_irc_netmask, we just try that and return self if it works.
\r
981 # Subclasses of User will return self if possible.
\r
983 def to_irc_user(opts={})
\r
984 return self if fits_with_server_and_casemap?(opts)
\r
985 return self.full_downcase.to_irc_user(opts)
\r
988 # We can replace everything at once with data from another User
\r
993 self.nick = other.nick
\r
994 self.user = other.user
\r
995 self.host = other.host
\r
996 @server = other.server
\r
997 @casemap = other.casemap unless @server
\r
998 @away = other.away?
\r
1000 self.replace(other.to_irc_user(server_and_casemap))
\r
1004 def modes_on(channel)
\r
1007 channel.modes_of(self)
\r
1009 return @server.channel(channel).modes_of(self) if @server
\r
1010 raise "Can't resolve channel #{channel}"
\r
1014 def is_op?(channel)
\r
1017 channel.has_op?(self)
\r
1019 return @server.channel(channel).has_op?(self) if @server
\r
1020 raise "Can't resolve channel #{channel}"
\r
1024 def is_voice?(channel)
\r
1027 channel.has_voice?(self)
\r
1029 return @server.channel(channel).has_voice?(self) if @server
\r
1030 raise "Can't resolve channel #{channel}"
\r
1036 # A UserList is an ArrayOf <code>User</code>s
\r
1037 # We derive it from NetmaskList, which allows us to inherit any special
\r
1038 # NetmaskList method
\r
1040 class UserList < NetmaskList
\r
1042 # Create a new UserList, optionally filling it with the elements from
\r
1043 # the Array argument fed to it.
\r
1045 def initialize(ar=[])
\r
1047 @element_class = User
\r
1050 # Convenience method: convert the UserList to a list of nicks. The indices
\r
1054 self.map { |user| user.nick }
\r
1063 # We keep extending String, this time adding a method that converts a
\r
1064 # String into an Irc::User object
\r
1066 def to_irc_user(opts={})
\r
1067 Irc::User.new(self, opts)
\r
1074 # An IRC Channel is identified by its name, and it has a set of properties:
\r
1075 # * a Channel::Topic
\r
1077 # * a set of Channel::Modes
\r
1079 # The Channel::Topic and Channel::Mode classes are defined within the
\r
1080 # Channel namespace because they only make sense there
\r
1085 # Mode on a Channel
\r
1088 attr_reader :channel
\r
1089 def initialize(ch)
\r
1096 # Channel modes of type A manipulate lists
\r
1098 # Example: b (banlist)
\r
1100 class ModeTypeA < Mode
\r
1102 def initialize(ch)
\r
1104 @list = NetmaskList.new
\r
1108 nm = @channel.server.new_netmask(val)
\r
1109 @list << nm unless @list.include?(nm)
\r
1113 nm = @channel.server.new_netmask(val)
\r
1120 # Channel modes of type B need an argument
\r
1122 # Example: k (key)
\r
1124 class ModeTypeB < Mode
\r
1125 def initialize(ch)
\r
1133 alias :value :status
\r
1140 @arg = nil if @arg == val
\r
1146 # Channel modes that change the User prefixes are like
\r
1147 # Channel modes of type B, except that they manipulate
\r
1148 # lists of Users, so they are somewhat similar to channel
\r
1151 class UserMode < ModeTypeB
\r
1153 alias :users :list
\r
1154 def initialize(ch)
\r
1156 @list = UserList.new
\r
1160 u = @channel.server.user(val)
\r
1161 @list << u unless @list.include?(u)
\r
1165 u = @channel.server.user(val)
\r
1172 # Channel modes of type C need an argument when set,
\r
1173 # but not when they get reset
\r
1175 # Example: l (limit)
\r
1177 class ModeTypeC < Mode
\r
1178 def initialize(ch)
\r
1186 alias :value :status
\r
1199 # Channel modes of type D are basically booleans
\r
1201 # Example: m (moderate)
\r
1203 class ModeTypeD < Mode
\r
1204 def initialize(ch)
\r
1224 # A Topic represents the topic of a channel. It consists of
\r
1225 # the topic itself, who set it and when
\r
1228 attr_accessor :text, :set_by, :set_on
\r
1231 # Create a new Topic setting the text, the creator and
\r
1232 # the creation time
\r
1234 def initialize(text="", set_by="", set_on=Time.new)
\r
1236 @set_by = set_by.to_irc_netmask
\r
1240 # Replace a Topic with another one
\r
1242 def replace(topic)
\r
1243 raise TypeError, "#{topic.inspect} is not of class #{self.class}" unless topic.kind_of?(self.class)
\r
1244 @text = topic.text.dup
\r
1245 @set_by = topic.set_by.dup
\r
1246 @set_on = topic.set_on.dup
\r
1251 def to_irc_channel_topic
\r
1264 # Returns an Irc::Channel::Topic with self as text
\r
1266 def to_irc_channel_topic
\r
1267 Irc::Channel::Topic.new(self)
\r
1276 # Here we start with the actual Channel class
\r
1280 include ServerOrCasemap
\r
1281 attr_reader :name, :topic, :mode, :users
\r
1285 str = self.__to_s__[0..-2]
\r
1286 str << " on server #{server}" if server
\r
1287 str << " @name=#{@name.inspect} @topic=#{@topic.text.inspect}"
\r
1288 str << " @users=[#{user_nicks.sort.join(', ')}]"
\r
1294 def to_irc_channel
\r
1300 @users.map { |u| u.downcase }
\r
1303 # Checks if the receiver already has a user with the given _nick_
\r
1305 def has_user?(nick)
\r
1306 @users.index(nick.to_irc_user(server_and_casemap))
\r
1309 # Returns the user with nick _nick_, if available
\r
1311 def get_user(nick)
\r
1312 idx = has_user?(nick)
\r
1313 @users[idx] if idx
\r
1316 # Adds a user to the channel
\r
1318 def add_user(user, opts={})
\r
1319 silent = opts.fetch(:silent, false)
\r
1320 if has_user?(user)
\r
1321 warn "Trying to add user #{user} to channel #{self} again" unless silent
\r
1323 @users << user.to_irc_user(server_and_casemap)
\r
1327 # Creates a new channel with the given name, optionally setting the topic
\r
1328 # and an initial users list.
\r
1330 # No additional info is created here, because the channel flags and userlists
\r
1331 # allowed depend on the server.
\r
1333 def initialize(name, topic=nil, users=[], opts={})
\r
1334 raise ArgumentError, "Channel name cannot be empty" if name.to_s.empty?
\r
1335 warn "Unknown channel prefix #{name[0].chr}" if name !~ /^[&#+!]/
\r
1336 raise ArgumentError, "Invalid character in #{name.inspect}" if name =~ /[ \x07,]/
\r
1338 init_server_or_casemap(opts)
\r
1342 @topic = topic ? topic.to_irc_channel_topic : Channel::Topic.new
\r
1344 @users = UserList.new
\r
1354 # Removes a user from the channel
\r
1356 def delete_user(user)
\r
1357 @mode.each { |sym, mode|
\r
1358 mode.reset(user) if mode.kind_of?(UserMode)
\r
1360 @users.delete(user)
\r
1363 # The channel prefix
\r
1369 # A channel is local to a server if it has the '&' prefix
\r
1375 # A channel is modeless if it has the '+' prefix
\r
1381 # A channel is safe if it has the '!' prefix
\r
1387 # A channel is normal if it has the '#' prefix
\r
1393 # Create a new mode
\r
1395 def create_mode(sym, kl)
\r
1396 @mode[sym.to_sym] = kl.new(self)
\r
1399 def modes_of(user)
\r
1401 @mode.map { |s, m|
\r
1402 l << s if (m.class <= UserMode and m.list[user])
\r
1408 @mode.has_key?(:o) and @mode[:o].list[user]
\r
1411 def has_voice?(user)
\r
1412 @mode.has_key?(:v) and @mode[:v].list[user]
\r
1417 # A ChannelList is an ArrayOf <code>Channel</code>s
\r
1419 class ChannelList < ArrayOf
\r
1421 # Create a new ChannelList, optionally filling it with the elements from
\r
1422 # the Array argument fed to it.
\r
1424 def initialize(ar=[])
\r
1425 super(Channel, ar)
\r
1428 # Convenience method: convert the ChannelList to a list of channel names.
\r
1429 # The indices are preserved
\r
1432 self.map { |chan| chan.name }
\r
1442 # We keep extending String, this time adding a method that converts a
\r
1443 # String into an Irc::Channel object
\r
1445 def to_irc_channel(opts={})
\r
1446 Irc::Channel.new(self, opts)
\r
1455 # An IRC Server represents the Server the client is connected to.
\r
1459 attr_reader :hostname, :version, :usermodes, :chanmodes
\r
1460 alias :to_s :hostname
\r
1461 attr_reader :supports, :capabilities
\r
1463 attr_reader :channels, :users
\r
1467 @channels.map { |ch| ch.downcase }
\r
1472 @users.map { |u| u.downcase }
\r
1476 chans, users = [@channels, @users].map {|d|
\r
1478 a.downcase <=> b.downcase
\r
1484 str = self.__to_s__[0..-2]
\r
1485 str << " @hostname=#{hostname}"
\r
1486 str << " @channels=#{chans}"
\r
1487 str << " @users=#{users}"
\r
1491 # Create a new Server, with all instance variables reset to nil (for
\r
1492 # scalar variables), empty channel and user lists and @supports
\r
1493 # initialized to the default values for all known supported features.
\r
1496 @hostname = @version = @usermodes = @chanmodes = nil
\r
1498 @channels = ChannelList.new
\r
1500 @users = UserList.new
\r
1502 reset_capabilities
\r
1505 # Resets the server capabilities
\r
1507 def reset_capabilities
\r
1509 :casemapping => 'rfc1459'.to_irc_casemap,
\r
1512 :typea => nil, # Type A: address lists
\r
1513 :typeb => nil, # Type B: needs a parameter
\r
1514 :typec => nil, # Type C: needs a parameter when set
\r
1515 :typed => nil # Type D: must not have a parameter
\r
1517 :channellen => 50,
\r
1518 :chantypes => "#&!+",
\r
1528 :modes => [:o, :v],
\r
1529 :prefixes => [:"@", :+]
\r
1532 :statusmsg => nil,
\r
1537 @capabilities = {}
\r
1540 # Convert a mode (o, v, h, ...) to the corresponding
\r
1541 # prefix (@, +, %, ...). See also mode_for_prefix
\r
1542 def prefix_for_mode(mode)
\r
1543 return @supports[:prefix][:prefixes][
\r
1544 @supports[:prefix][:modes].index(mode.to_sym)
\r
1548 # Convert a prefix (@, +, %, ...) to the corresponding
\r
1549 # mode (o, v, h, ...). See also prefix_for_mode
\r
1550 def mode_for_prefix(pfx)
\r
1551 return @supports[:prefix][:modes][
\r
1552 @supports[:prefix][:prefixes].index(pfx.to_sym)
\r
1556 # Resets the Channel and User list
\r
1559 @users.reverse_each { |u|
\r
1562 @channels.reverse_each { |u|
\r
1567 # Clears the server
\r
1571 reset_capabilities
\r
1572 @hostname = @version = @usermodes = @chanmodes = nil
\r
1575 # This method is used to parse a 004 RPL_MY_INFO line
\r
1577 def parse_my_info(line)
\r
1578 ar = line.split(' ')
\r
1581 @usermodes = ar[2]
\r
1582 @chanmodes = ar[3]
\r
1585 def noval_warn(key, val, &block)
\r
1587 yield if block_given?
\r
1589 warn "No #{key.to_s.upcase} value"
\r
1593 def val_warn(key, val, &block)
\r
1594 if val == true or val == false or val.nil?
\r
1595 yield if block_given?
\r
1597 warn "No #{key.to_s.upcase} value must be specified, got #{val}"
\r
1600 private :noval_warn, :val_warn
\r
1602 # This method is used to parse a 005 RPL_ISUPPORT line
\r
1604 # See the RPL_ISUPPORT draft[http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt]
\r
1606 def parse_isupport(line)
\r
1607 debug "Parsing ISUPPORT #{line.inspect}"
\r
1608 ar = line.split(' ')
\r
1611 prekey, val = en.split('=', 2)
\r
1612 if prekey =~ /^-(.*)/
\r
1613 key = $1.downcase.to_sym
\r
1616 key = prekey.downcase.to_sym
\r
1620 noval_warn(key, val) {
\r
1621 @supports[key] = val.to_irc_casemap
\r
1623 when :chanlimit, :idchan, :maxlist, :targmax
\r
1624 noval_warn(key, val) {
\r
1625 groups = val.split(',')
\r
1627 k, v = g.split(':')
\r
1628 @supports[key][k] = v.to_i || 0
\r
1629 if @supports[key][k] == 0
\r
1630 warn "Deleting #{key} limit of 0 for #{k}"
\r
1631 @supports[key].delete(k)
\r
1636 noval_warn(key, val) {
\r
1637 groups = val.split(',')
\r
1638 @supports[key][:typea] = groups[0].scan(/./).map { |x| x.to_sym}
\r
1639 @supports[key][:typeb] = groups[1].scan(/./).map { |x| x.to_sym}
\r
1640 @supports[key][:typec] = groups[2].scan(/./).map { |x| x.to_sym}
\r
1641 @supports[key][:typed] = groups[3].scan(/./).map { |x| x.to_sym}
\r
1643 when :channellen, :kicklen, :modes, :topiclen
\r
1645 @supports[key] = val.to_i
\r
1647 @supports[key] = nil
\r
1650 @supports[key] = val # can also be nil
\r
1653 @supports[key] = val
\r
1656 @supports[key] = val
\r
1658 noval_warn(key, val) {
\r
1659 reparse += "CHANLIMIT=(chantypes):#{val} "
\r
1662 noval_warn(key, val) {
\r
1663 @supports[:targmax]['PRIVMSG'] = val.to_i
\r
1664 @supports[:targmax]['NOTICE'] = val.to_i
\r
1667 noval_warn(key, val) {
\r
1668 @supports[key] = val
\r
1671 noval_warn(key, val) {
\r
1672 @supports[key] = val.to_i
\r
1676 val.scan(/\((.*)\)(.*)/) { |m, p|
\r
1677 @supports[key][:modes] = m.scan(/./).map { |x| x.to_sym}
\r
1678 @supports[key][:prefixes] = p.scan(/./).map { |x| x.to_sym}
\r
1681 @supports[key][:modes] = nil
\r
1682 @supports[key][:prefixes] = nil
\r
1685 val_warn(key, val) {
\r
1686 @supports[key] = val.nil? ? true : val
\r
1689 noval_warn(key, val) {
\r
1690 @supports[key] = val.scan(/./)
\r
1693 noval_warn(key, val) {
\r
1694 @supports[key] = val.split(',')
\r
1697 @supports[key] = val.nil? ? true : val
\r
1700 reparse.gsub!("(chantypes)",@supports[:chantypes])
\r
1701 parse_isupport(reparse) unless reparse.empty?
\r
1704 # Returns the casemap of the server.
\r
1707 @supports[:casemapping]
\r
1710 # Returns User or Channel depending on what _name_ can be
\r
1713 def user_or_channel?(name)
\r
1714 if supports[:chantypes].include?(name[0])
\r
1721 # Returns the actual User or Channel object matching _name_
\r
1723 def user_or_channel(name)
\r
1724 if supports[:chantypes].include?(name[0])
\r
1725 return channel(name)
\r
1731 # Checks if the receiver already has a channel with the given _name_
\r
1733 def has_channel?(name)
\r
1734 return false if name.nil_or_empty?
\r
1735 channel_names.index(name.irc_downcase(casemap))
\r
1737 alias :has_chan? :has_channel?
\r
1739 # Returns the channel with name _name_, if available
\r
1741 def get_channel(name)
\r
1742 return nil if name.nil_or_empty?
\r
1743 idx = has_channel?(name)
\r
1744 channels[idx] if idx
\r
1746 alias :get_chan :get_channel
\r
1748 # Create a new Channel object bound to the receiver and add it to the
\r
1749 # list of <code>Channel</code>s on the receiver, unless the channel was
\r
1750 # present already. In this case, the default action is to raise an
\r
1751 # exception, unless _fails_ is set to false. An exception can also be
\r
1752 # raised if _str_ is nil or empty, again only if _fails_ is set to true;
\r
1753 # otherwise, the method just returns nil
\r
1755 def new_channel(name, topic=nil, users=[], fails=true)
\r
1756 if name.nil_or_empty?
\r
1757 raise "Tried to look for empty or nil channel name #{name.inspect}" if fails
\r
1760 ex = get_chan(name)
\r
1762 raise "Channel #{name} already exists on server #{self}" if fails
\r
1766 prefix = name[0].chr
\r
1768 # Give a warning if the new Channel goes over some server limits.
\r
1770 # FIXME might need to raise an exception
\r
1772 warn "#{self} doesn't support channel prefix #{prefix}" unless @supports[:chantypes].include?(prefix)
\r
1773 warn "#{self} doesn't support channel names this long (#{name.length} > #{@supports[:channellen]})" unless name.length <= @supports[:channellen]
\r
1775 # Next, we check if we hit the limit for channels of type +prefix+
\r
1776 # if the server supports +chanlimit+
\r
1778 @supports[:chanlimit].keys.each { |k|
\r
1779 next unless k.include?(prefix)
\r
1781 channel_names.each { |n|
\r
1782 count += 1 if k.include?(n[0])
\r
1784 # raise IndexError, "Already joined #{count} channels with prefix #{k}" if count == @supports[:chanlimit][k]
\r
1785 warn "Already joined #{count}/#{@supports[:chanlimit][k]} channels with prefix #{k}, we may be going over server limits" if count >= @supports[:chanlimit][k]
\r
1788 # So far, everything is fine. Now create the actual Channel
\r
1790 chan = Channel.new(name, topic, users, :server => self)
\r
1792 # We wade through +prefix+ and +chanmodes+ to create appropriate
\r
1793 # lists and flags for this channel
\r
1795 @supports[:prefix][:modes].each { |mode|
\r
1796 chan.create_mode(mode, Channel::UserMode)
\r
1797 } if @supports[:prefix][:modes]
\r
1799 @supports[:chanmodes].each { |k, val|
\r
1804 chan.create_mode(mode, Channel::ModeTypeA)
\r
1808 chan.create_mode(mode, Channel::ModeTypeB)
\r
1812 chan.create_mode(mode, Channel::ModeTypeC)
\r
1816 chan.create_mode(mode, Channel::ModeTypeD)
\r
1823 # debug "Created channel #{chan.inspect}"
\r
1828 # Returns the Channel with the given _name_ on the server,
\r
1829 # creating it if necessary. This is a short form for
\r
1830 # new_channel(_str_, nil, [], +false+)
\r
1833 new_channel(str,nil,[],false)
\r
1836 # Remove Channel _name_ from the list of <code>Channel</code>s
\r
1838 def delete_channel(name)
\r
1839 idx = has_channel?(name)
\r
1840 raise "Tried to remove unmanaged channel #{name}" unless idx
\r
1841 @channels.delete_at(idx)
\r
1844 # Checks if the receiver already has a user with the given _nick_
\r
1846 def has_user?(nick)
\r
1847 return false if nick.nil_or_empty?
\r
1848 user_nicks.index(nick.irc_downcase(casemap))
\r
1851 # Returns the user with nick _nick_, if available
\r
1853 def get_user(nick)
\r
1854 idx = has_user?(nick)
\r
1855 @users[idx] if idx
\r
1858 # Create a new User object bound to the receiver and add it to the list
\r
1859 # of <code>User</code>s on the receiver, unless the User was present
\r
1860 # already. In this case, the default action is to raise an exception,
\r
1861 # unless _fails_ is set to false. An exception can also be raised
\r
1862 # if _str_ is nil or empty, again only if _fails_ is set to true;
\r
1863 # otherwise, the method just returns nil
\r
1865 def new_user(str, fails=true)
\r
1866 if str.nil_or_empty?
\r
1867 raise "Tried to look for empty or nil user name #{str.inspect}" if fails
\r
1870 tmp = str.to_irc_user(:server => self)
\r
1871 old = get_user(tmp.nick)
\r
1872 # debug "Tmp: #{tmp.inspect}"
\r
1873 # debug "Old: #{old.inspect}"
\r
1875 # debug "User already existed as #{old.inspect}"
\r
1878 # debug "Both were known"
\r
1879 # Do not raise an error: things like Freenode change the hostname after identification
\r
1880 warning "User #{tmp.nick} has inconsistent Netmasks! #{self} knows #{old.inspect} but access was tried with #{tmp.inspect}" if old != tmp
\r
1881 raise "User #{tmp} already exists on server #{self}" if fails
\r
1883 if old.fullform.downcase != tmp.fullform.downcase
\r
1885 # debug "Known user now #{old.inspect}"
\r
1890 warn "#{self} doesn't support nicknames this long (#{tmp.nick.length} > #{@supports[:nicklen]})" unless tmp.nick.length <= @supports[:nicklen]
\r
1892 return @users.last
\r
1896 # Returns the User with the given Netmask on the server,
\r
1897 # creating it if necessary. This is a short form for
\r
1898 # new_user(_str_, +false+)
\r
1901 new_user(str, false)
\r
1904 # Deletes User _user_ from Channel _channel_
\r
1906 def delete_user_from_channel(user, channel)
\r
1907 channel.delete_user(user)
\r
1910 # Remove User _someuser_ from the list of <code>User</code>s.
\r
1911 # _someuser_ must be specified with the full Netmask.
\r
1913 def delete_user(someuser)
\r
1914 idx = has_user?(someuser)
\r
1915 raise "Tried to remove unmanaged user #{user}" unless idx
\r
1916 have = self.user(someuser)
\r
1917 @channels.each { |ch|
\r
1918 delete_user_from_channel(have, ch)
\r
1920 @users.delete_at(idx)
\r
1923 # Create a new Netmask object with the appropriate casemap
\r
1925 def new_netmask(str)
\r
1926 str.to_irc_netmask(:server => self)
\r
1929 # Finds all <code>User</code>s on server whose Netmask matches _mask_
\r
1931 def find_users(mask)
\r
1932 nm = new_netmask(mask)
\r
1933 @users.inject(UserList.new) {
\r
1935 if user.user == "*" or user.host == "*"
\r
1936 list << user if user.nick.irc_downcase(casemap) =~ nm.nick.irc_downcase(casemap).to_irc_regexp
\r
1938 list << user if user.matches?(nm)
\r