3 # * do we want to handle a Channel list for each User telling which
4 # Channels is the User on (of those the client is on too)?
5 # We may want this so that when a User leaves all Channels and he hasn't
6 # sent us privmsgs, we know we can remove him from the Server @users list
7 # FIXME for the time being, we do it with a method that scans the server
8 # (if defined), so the method is slow and should not be used frequently.
9 # * Maybe ChannelList and UserList should be HashesOf instead of ArrayOf?
10 # See items marked as TODO Ho.
11 # The framework to do this is now in place, thanks to the new [] method
12 # for NetmaskList, which allows retrieval by Netmask or String
18 # This module defines the fundamental building blocks for IRC
20 # Author:: Giuseppe Bilotta (giuseppe.bilotta@gmail.com)
24 # The following monkeypatch is to fix a bug in Singleton where marshaling would
25 # fail when trying to restore a marshaled Singleton due to _load being declared
27 if RUBY_VERSION < '1.9'
33 module SingletonClassMethods
41 # We extend the Object class with a method that
42 # checks if the receiver is nil or empty
44 return true unless self
45 return true if self.respond_to? :empty? and self.empty?
49 # We alias the to_s method to __to_s__ to make
50 # it accessible in all classes
54 # The Irc module is used to keep all IRC-related classes
55 # in the same namespace
60 # Due to its Scandinavian origins, IRC has strange case mappings, which
61 # consider the characters <tt>{}|^</tt> as the uppercase
62 # equivalents of # <tt>[]\~</tt>.
64 # This is however not the same on all IRC servers: some use standard ASCII
65 # casemapping, other do not consider <tt>^</tt> as the uppercase of
71 # Create a new casemap with name _name_, uppercase characters _upper_ and
72 # lowercase characters _lower_
74 def initialize(name, upper, lower)
76 raise "Casemap #{name.inspect} already exists!" if @@casemaps.has_key?(@key)
84 # Returns the Casemap with the given name
87 @@casemaps[name.to_sym][:casemap]
90 # Retrieve the 'uppercase characters' of this Casemap
93 @@casemaps[@key][:upper]
96 # Retrieve the 'lowercase characters' of this Casemap
99 @@casemaps[@key][:lower]
102 # Return a Casemap based on the receiver
108 # A Casemap is represented by its lower/upper mappings
111 self.__to_s__[0..-2] + " #{upper.inspect} ~(#{self})~ #{lower.inspect}>"
114 # As a String we return our name
120 # Two Casemaps are equal if they have the same upper and lower ranges
123 other = arg.to_irc_casemap
124 return self.upper == other.upper && self.lower == other.lower
127 # Give a warning if _arg_ and self are not the same Casemap
130 other = arg.to_irc_casemap
134 warn "Casemap mismatch (#{self.inspect} != #{other.inspect})"
141 # The rfc1459 casemap
143 class RfcCasemap < Casemap
147 super('rfc1459', "\x41-\x5a\x7b-\x7e", "\x61-\x7a\x5b-\x5e")
153 # The strict-rfc1459 Casemap
155 class StrictRfcCasemap < Casemap
159 super('strict-rfc1459', "\x41-\x5a\x7b-\x7d", "\x61-\x7a\x5b-\x5d")
163 StrictRfcCasemap.instance
167 class AsciiCasemap < Casemap
171 super('ascii', "\x41-\x5a", "\x61-\x7a")
175 AsciiCasemap.instance
178 # This module is included by all classes that are either bound to a server
179 # or should have a casemap.
181 module ServerOrCasemap
185 # This method initializes the instance variables @server and @casemap
186 # according to the values of the hash keys :server and :casemap in _opts_
188 def init_server_or_casemap(opts={})
189 @server = opts.fetch(:server, nil)
190 raise TypeError, "#{@server} is not a valid Irc::Server" if @server and not @server.kind_of?(Server)
192 @casemap = opts.fetch(:casemap, nil)
195 @server.casemap.must_be(@casemap)
199 @casemap = (@casemap || 'rfc1459').to_irc_casemap
203 # This is an auxiliary method: it returns true if the receiver fits the
204 # server and casemap specified in _opts_, false otherwise.
206 def fits_with_server_and_casemap?(opts={})
207 srv = opts.fetch(:server, nil)
208 cmap = opts.fetch(:casemap, nil)
209 cmap = cmap.to_irc_casemap unless cmap.nil?
212 return true if cmap.nil? or cmap == casemap
214 return true if srv == @server and (cmap.nil? or cmap == casemap)
219 # Returns the casemap of the receiver, by looking at the bound
220 # @server (if possible) or at the @casemap otherwise
223 return @server.casemap if defined?(@server) and @server
227 # Returns a hash with the current @server and @casemap as values of
228 # :server and :casemap
230 def server_and_casemap
232 h[:server] = @server if defined?(@server) and @server
233 h[:casemap] = @casemap if defined?(@casemap) and @casemap
237 # We allow up/downcasing with a different casemap
239 def irc_downcase(cmap=casemap)
240 self.to_s.irc_downcase(cmap)
243 # Up/downcasing something that includes this module returns its
244 # Up/downcased to_s form
250 # We allow up/downcasing with a different casemap
252 def irc_upcase(cmap=casemap)
253 self.to_s.irc_upcase(cmap)
256 # Up/downcasing something that includes this module returns its
257 # Up/downcased to_s form
268 # We start by extending the String class
269 # with some IRC-specific methods
273 # This method returns the Irc::Casemap whose name is the receiver
277 Irc::Casemap.get(self)
279 # raise TypeError, "Unkown Irc::Casemap #{self.inspect}"
280 error "Unkown Irc::Casemap #{self.inspect} requested, defaulting to rfc1459"
281 Irc::Casemap.get('rfc1459')
285 # This method returns a string which is the downcased version of the
286 # receiver, according to the given _casemap_
289 def irc_downcase(casemap='rfc1459')
290 cmap = casemap.to_irc_casemap
291 self.tr(cmap.upper, cmap.lower)
294 # This is the same as the above, except that the string is altered in place
296 # See also the discussion about irc_downcase
298 def irc_downcase!(casemap='rfc1459')
299 cmap = casemap.to_irc_casemap
300 self.tr!(cmap.upper, cmap.lower)
303 # Upcasing functions are provided too
305 # See also the discussion about irc_downcase
307 def irc_upcase(casemap='rfc1459')
308 cmap = casemap.to_irc_casemap
309 self.tr(cmap.lower, cmap.upper)
314 # See also the discussion about irc_downcase
316 def irc_upcase!(casemap='rfc1459')
317 cmap = casemap.to_irc_casemap
318 self.tr!(cmap.lower, cmap.upper)
321 # This method checks if the receiver contains IRC glob characters
323 # IRC has a very primitive concept of globs: a <tt>*</tt> stands for "any
324 # number of arbitrary characters", a <tt>?</tt> stands for "one and exactly
325 # one arbitrary character". These characters can be escaped by prefixing them
326 # with a slash (<tt>\\</tt>).
328 # A known limitation of this glob syntax is that there is no way to escape
329 # the escape character itself, so it's not possible to build a glob pattern
330 # where the escape character precedes a glob.
333 self =~ /^[*?]|[^\\][*?]/
336 # This method is used to convert the receiver into a Regular Expression
337 # that matches according to the IRC glob syntax
340 regmask = Regexp.escape(self)
341 regmask.gsub!(/(\\\\)?\\[*?]/) { |m|
350 raise "Unexpected match #{m} when converting #{self}"
353 Regexp.new("^#{regmask}$")
359 # ArrayOf is a subclass of Array whose elements are supposed to be all
360 # of the same class. This is not intended to be used directly, but rather
361 # to be subclassed as needed (see for example Irc::UserList and Irc::NetmaskList)
363 # Presently, only very few selected methods from Array are overloaded to check
364 # if the new elements are the correct class. An orthodox? method is provided
365 # to check the entire ArrayOf against the appropriate class.
367 class ArrayOf < Array
369 attr_reader :element_class
371 # Create a new ArrayOf whose elements are supposed to be all of type _kl_,
372 # optionally filling it with the elements from the Array argument.
374 def initialize(kl, ar=[])
375 raise TypeError, "#{kl.inspect} must be a class name" unless kl.kind_of?(Class)
382 raise TypeError, "#{self.class} can only be initialized from an Array"
387 self.__to_s__[0..-2].sub(/:[^:]+$/,"[#{@element_class}]\\0") + " #{super}>"
390 # Private method to check the validity of the elements passed to it
391 # and optionally raise an error
393 # TODO should it accept nils as valid?
395 def internal_will_accept?(raising, *els)
397 unless el.kind_of?(@element_class)
398 raise TypeError, "#{el.inspect} is not of class #{@element_class}" if raising
404 private :internal_will_accept?
406 # This method checks if the passed arguments are acceptable for our ArrayOf
408 def will_accept?(*els)
409 internal_will_accept?(false, *els)
412 # This method checks that all elements are of the appropriate class
418 # This method is similar to the above, except that it raises an exception
419 # if the receiver is not valid
422 raise TypeError unless valid?
425 # Overloaded from Array#<<, checks for appropriate class of argument
428 super(el) if internal_will_accept?(true, el)
431 # Overloaded from Array#&, checks for appropriate class of argument elements
435 ArrayOf.new(@element_class, r) if internal_will_accept?(true, *r)
438 # Overloaded from Array#+, checks for appropriate class of argument elements
441 ArrayOf.new(@element_class, super(ar)) if internal_will_accept?(true, *ar)
444 # Overloaded from Array#-, so that an ArrayOf is returned. There is no need
445 # to check the validity of the elements in the argument
448 ArrayOf.new(@element_class, super(ar)) # if internal_will_accept?(true, *ar)
451 # Overloaded from Array#|, checks for appropriate class of argument elements
454 ArrayOf.new(@element_class, super(ar)) if internal_will_accept?(true, *ar)
457 # Overloaded from Array#concat, checks for appropriate class of argument
461 super(ar) if internal_will_accept?(true, *ar)
464 # Overloaded from Array#insert, checks for appropriate class of argument
468 super(idx, *ar) if internal_will_accept?(true, *ar)
471 # Overloaded from Array#replace, checks for appropriate class of argument
475 super(ar) if (ar.kind_of?(ArrayOf) && ar.element_class <= @element_class) or internal_will_accept?(true, *ar)
478 # Overloaded from Array#push, checks for appropriate class of argument
482 super(*ar) if internal_will_accept?(true, *ar)
485 # Overloaded from Array#unshift, checks for appropriate class of argument(s)
489 super(el) if internal_will_accept?(true, *els)
493 # We introduce the 'downcase' method, which maps downcase() to all the Array
494 # elements, properly failing when the elements don't have a downcase method
497 self.map { |el| el.downcase }
500 # Modifying methods which we don't handle yet are made private
502 private :[]=, :collect!, :map!, :fill, :flatten!
507 # We extend the Regexp class with an Irc module which will contain some
508 # Irc-specific regexps
512 # We start with some general-purpose ones which will be used in the
513 # Irc module too, but are useful regardless
515 HEX_DIGIT = /[0-9A-Fa-f]/
516 HEX_DIGITS = /#{HEX_DIGIT}+/
517 HEX_OCTET = /#{HEX_DIGIT}#{HEX_DIGIT}?/
518 DEC_OCTET = /[01]?\d?\d|2[0-4]\d|25[0-5]/
519 DEC_IP_ADDR = /#{DEC_OCTET}\.#{DEC_OCTET}\.#{DEC_OCTET}\.#{DEC_OCTET}/
520 HEX_IP_ADDR = /#{HEX_OCTET}\.#{HEX_OCTET}\.#{HEX_OCTET}\.#{HEX_OCTET}/
521 IP_ADDR = /#{DEC_IP_ADDR}|#{HEX_IP_ADDR}/
523 # IPv6, from Resolv::IPv6, without the \A..\z anchors
524 HEX_16BIT = /#{HEX_DIGIT}{1,4}/
525 IP6_8Hex = /(?:#{HEX_16BIT}:){7}#{HEX_16BIT}/
526 IP6_CompressedHex = /((?:#{HEX_16BIT}(?::#{HEX_16BIT})*)?)::((?:#{HEX_16BIT}(?::#{HEX_16BIT})*)?)/
527 IP6_6Hex4Dec = /((?:#{HEX_16BIT}:){6,6})#{DEC_IP_ADDR}/
528 IP6_CompressedHex4Dec = /((?:#{HEX_16BIT}(?::#{HEX_16BIT})*)?)::((?:#{HEX_16BIT}:)*)#{DEC_IP_ADDR}/
529 IP6_ADDR = /(?:#{IP6_8Hex})|(?:#{IP6_CompressedHex})|(?:#{IP6_6Hex4Dec})|(?:#{IP6_CompressedHex4Dec})/
531 # We start with some IRC related regular expressions, used to match
532 # Irc::User nicks and users and Irc::Channel names
534 # For each of them we define two versions of the regular expression:
535 # * a generic one, which should match for any server but may turn out to
536 # match more than a specific server would accept
537 # * an RFC-compliant matcher
541 # Channel-name-matching regexps
543 CHAN_SAFE = /![A-Z0-9]{5}/
544 CHAN_ANY = /[^\x00\x07\x0A\x0D ,:]/
545 GEN_CHAN = /(?:#{CHAN_FIRST}|#{CHAN_SAFE})#{CHAN_ANY}+/
546 RFC_CHAN = /#{CHAN_FIRST}#{CHAN_ANY}{1,49}|#{CHAN_SAFE}#{CHAN_ANY}{1,44}/
548 # Nick-matching regexps
549 SPECIAL_CHAR = /[\x5b-\x60\x7b-\x7d]/
550 NICK_FIRST = /#{SPECIAL_CHAR}|[[:alpha:]]/
551 NICK_ANY = /#{SPECIAL_CHAR}|[[:alnum:]]|-/
552 GEN_NICK = /#{NICK_FIRST}#{NICK_ANY}+/
553 RFC_NICK = /#{NICK_FIRST}#{NICK_ANY}{0,8}/
555 USER_CHAR = /[^\x00\x0a\x0d @]/
556 GEN_USER = /#{USER_CHAR}+/
558 # Host-matching regexps
559 HOSTNAME_COMPONENT = /[[:alnum:]](?:[[:alnum:]]|-)*[[:alnum:]]*/
560 HOSTNAME = /#{HOSTNAME_COMPONENT}(?:\.#{HOSTNAME_COMPONENT})*/
561 HOSTADDR = /#{IP_ADDR}|#{IP6_ADDR}/
563 GEN_HOST = /#{HOSTNAME}|#{HOSTADDR}/
565 # # FreeNode network replaces the host of affiliated users with
567 # # FIXME we need the true syntax to match it properly ...
568 # PDPC_HOST_PART = /[0-9A-Za-z.-]+/
569 # PDPC_HOST = /#{PDPC_HOST_PART}(?:\/#{PDPC_HOST_PART})+/
571 # # NOTE: the final optional and non-greedy dot is needed because some
572 # # servers (e.g. FreeNode) send the hostname of the services as "services."
573 # # which is not RFC compliant, but sadly done.
574 # GEN_HOST_EXT = /#{PDPC_HOST}|#{GEN_HOST}\.??/
576 # Sadly, different networks have different, RFC-breaking ways of cloaking
577 # the actualy host address: see above for an example to handle FreeNode.
578 # Another example would be Azzurra, wich also inserts a "=" in the
579 # cloacked host. So let's just not care about this and go with the simplest
583 # User-matching Regexp
584 GEN_USER_ID = /(#{GEN_NICK})(?:(?:!(#{GEN_USER}))?@(#{GEN_HOST_EXT}))?/
586 # Things such has the BIP proxy send invalid nicks in a complete netmask,
587 # so we want to match this, rather: this matches either a compliant nick
588 # or a a string with a very generic nick, a very generic hostname after an
589 # @ sign, and an optional user after a !
590 BANG_AT = /#{GEN_NICK}|\S+?(?:!\S+?)?@\S+?/
592 # # For Netmask, we want to allow wildcards * and ? in the nick
593 # # (they are already allowed in the user and host part
594 # GEN_NICK_MASK = /(?:#{NICK_FIRST}|[?*])?(?:#{NICK_ANY}|[?*])+/
596 # # Netmask-matching Regexp
597 # GEN_MASK = /(#{GEN_NICK_MASK})(?:(?:!(#{GEN_USER}))?@(#{GEN_HOST_EXT}))?/
607 # A Netmask identifies each user by collecting its nick, username and
608 # hostname in the form <tt>nick!user@host</tt>
610 # Netmasks can also contain glob patterns in any of their components; in
611 # this form they are used to refer to more than a user or to a user
612 # appearing under different forms.
615 # * <tt>*!*@*</tt> refers to everybody
616 # * <tt>*!someuser@somehost</tt> refers to user +someuser+ on host +somehost+
617 # regardless of the nick used.
621 # Netmasks have an associated casemap unless they are bound to a server
623 include ServerOrCasemap
625 attr_reader :nick, :user, :host
628 # Create a new Netmask from string _str_, which must be in the form
629 # _nick_!_user_@_host_
631 # It is possible to specify a server or a casemap in the optional Hash:
632 # these are used to associate the Netmask with the given server and to set
633 # its casemap: if a server is specified and a casemap is not, the server's
634 # casemap is used. If both a server and a casemap are specified, the
635 # casemap must match the server's casemap or an exception will be raised.
637 # Empty +nick+, +user+ or +host+ are converted to the generic glob pattern
639 def initialize(str="", opts={})
640 # First of all, check for server/casemap option
642 init_server_or_casemap(opts)
644 # Now we can see if the given string _str_ is an actual Netmask
645 if str.respond_to?(:to_str)
647 # We match a pretty generic string, to work around non-compliant
649 when /^(?:(\S+?)(?:(?:!(\S+?))?@(\S+))?)?$/
650 # We do assignment using our internal methods
655 raise ArgumentError, "#{str.to_str.inspect} does not represent a valid #{self.class}"
658 raise TypeError, "#{str} cannot be converted to a #{self.class}"
662 # A Netmask is easily converted to a String for the usual representation.
663 # We skip the user or host parts if they are "*", unless we've been asked
668 ret << "!" << user unless user == "*"
669 ret << "@" << host unless host == "*"
674 "#{nick}!#{user}@#{host}"
677 alias :to_str :fullform
679 # This method downcases the fullform of the netmask. While this may not be
680 # significantly different from the #downcase() method provided by the
681 # ServerOrCasemap mixin, it's significantly different for Netmask
682 # subclasses such as User whose simple downcasing uses the nick only.
684 def full_irc_downcase(cmap=casemap)
685 self.fullform.irc_downcase(cmap)
688 # full_downcase() will return the fullform downcased according to the
692 self.full_irc_downcase
695 # This method returns a new Netmask which is the fully downcased version
698 return self.full_downcase.to_irc_netmask(server_and_casemap)
701 # Converts the receiver into a Netmask with the given (optional)
702 # server/casemap association. We return self unless a conversion
703 # is needed (different casemap/server)
705 # Subclasses of Netmask will return a new Netmask, using full_downcase
707 def to_irc_netmask(opts={})
708 if self.class == Netmask
709 return self if fits_with_server_and_casemap?(opts)
711 return self.full_downcase.to_irc_netmask(server_and_casemap.merge(opts))
714 # Converts the receiver into a User with the given (optional)
715 # server/casemap association. We return self unless a conversion
716 # is needed (different casemap/server)
718 def to_irc_user(opts={})
719 self.fullform.to_irc_user(server_and_casemap.merge(opts))
722 # Inspection of a Netmask reveals the server it's bound to (if there is
723 # one), its casemap and the nick, user and host part
726 str = self.__to_s__[0..-2]
727 str << " @server=#{@server}" if defined?(@server) and @server
728 str << " @nick=#{@nick.inspect} @user=#{@user.inspect}"
729 str << " @host=#{@host.inspect} casemap=#{casemap.inspect}"
733 # Equality: two Netmasks are equal if they downcase to the same thing
735 # TODO we may want it to try other.to_irc_netmask
738 return false unless other.kind_of?(self.class)
739 self.downcase == other.downcase
742 # This method changes the nick of the Netmask, defaulting to the generic
743 # glob pattern if the result is the null string.
747 @nick = "*" if @nick.empty?
750 # This method changes the user of the Netmask, defaulting to the generic
751 # glob pattern if the result is the null string.
755 @user = "*" if @user.empty?
759 # This method changes the hostname of the Netmask, defaulting to the generic
760 # glob pattern if the result is the null string.
764 @host = "*" if @host.empty?
767 # We can replace everything at once with data from another Netmask
775 @server = other.server
776 @casemap = other.casemap unless @server
778 replace(other.to_irc_netmask(server_and_casemap))
782 # This method checks if a Netmask is definite or not, by seeing if
783 # any of its components are defined by globs
786 return @nick.has_irc_glob? || @user.has_irc_glob? || @host.has_irc_glob?
791 unless u.has_irc_glob?
792 u.sub!(/^[in]=/, '=') or u.sub!(/^\W(\w+)/, '\1')
797 unless h.has_irc_glob?
799 h.sub!(/x-\w+$/, 'x-*')
801 h.match(/^[^\.]+\.[^\.]+$/) or
802 h.sub!(/azzurra[=-][0-9a-f]+/i, '*') or # hello, azzurra, you suck!
803 h.sub!(/^(\d+\.\d+\.\d+\.)\d+$/, '\1*') or
804 h.sub!(/^[^\.]+\./, '*.')
807 return Netmask.new("*!#{u}@#{h}", server_and_casemap)
810 # This method is used to match the current Netmask against another one
812 # The method returns true if each component of the receiver matches the
813 # corresponding component of the argument. By _matching_ here we mean
814 # that any netmask described by the receiver is also described by the
817 # In this sense, matching is rather simple to define in the case when the
818 # receiver has no globs: it is just necessary to check if the argument
819 # describes the receiver, which can be done by matching it against the
820 # argument converted into an IRC Regexp (see String#to_irc_regexp).
822 # The situation is also easy when the receiver has globs and the argument
823 # doesn't, since in this case the result is false.
825 # The more complex case in which both the receiver and the argument have
826 # globs is not handled yet.
829 cmp = arg.to_irc_netmask(:casemap => casemap)
830 debug "Matching #{self.fullform} against #{arg.inspect} (#{cmp.fullform})"
831 [:nick, :user, :host].each { |component|
832 us = self.send(component).irc_downcase(casemap)
833 them = cmp.send(component).irc_downcase(casemap)
834 if us.has_irc_glob? && them.has_irc_glob?
836 warn NotImplementedError
839 return false if us.has_irc_glob? && !them.has_irc_glob?
840 return false unless us =~ them.to_irc_regexp
845 # Case equality. Checks if arg matches self
848 arg.to_irc_netmask(:casemap => casemap).matches?(self)
851 # Sorting is done via the fullform
856 self.fullform.irc_downcase(casemap) <=> arg.fullform.irc_downcase(casemap)
858 self.downcase <=> arg.downcase
865 # A NetmaskList is an ArrayOf <code>Netmask</code>s
867 class NetmaskList < ArrayOf
869 # Create a new NetmaskList, optionally filling it with the elements from
870 # the Array argument fed to it.
872 def initialize(ar=[])
876 # We enhance the [] method by allowing it to pick an element that matches
877 # a given Netmask, a String or a Regexp
878 # TODO take into consideration the opportunity to use select() instead of
879 # find(), and/or a way to let the user choose which one to take (second
887 mask.matches?(args[0])
891 mask.matches?(args[0].to_irc_netmask(:casemap => mask.casemap))
895 mask.fullform =~ args[0]
912 # We keep extending String, this time adding a method that converts a
913 # String into an Irc::Netmask object
915 def to_irc_netmask(opts={})
916 Irc::Netmask.new(self, opts)
925 # An IRC User is identified by his/her Netmask (which must not have globs).
926 # In fact, User is just a subclass of Netmask.
928 # Ideally, the user and host information of an IRC User should never
929 # change, and it shouldn't contain glob patterns. However, IRC is somewhat
930 # idiosincratic and it may be possible to know the nick of a User much before
931 # its user and host are known. Moreover, some networks (namely Freenode) may
932 # change the hostname of a User when (s)he identifies with Nickserv.
934 # As a consequence, we must allow changes to a User host and user attributes.
935 # We impose a restriction, though: they may not contain glob patterns, except
936 # for the special case of an unknown user/host which is represented by a *.
938 # It is possible to create a totally unknown User (e.g. for initializations)
939 # by setting the nick to * too.
942 # * see if it's worth to add the other USER data
943 # * see if it's worth to add NICKSERV status
948 attr_accessor :real_name, :idle_since, :signon
950 # Create a new IRC User from a given Netmask (or anything that can be converted
951 # into a Netmask) provided that the given Netmask does not have globs.
953 def initialize(str="", opts={})
955 raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if nick.has_irc_glob? && nick != "*"
956 raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if user.has_irc_glob? && user != "*"
957 raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if host.has_irc_glob? && host != "*"
959 @real_name = String.new
964 # The nick of a User may be changed freely, but it must not contain glob patterns.
967 raise "Can't change the nick to #{newnick}" if defined?(@nick) and newnick.has_irc_glob?
971 # We have to allow changing the user of an Irc User due to some networks
972 # (e.g. Freenode) changing hostmasks on the fly. We still check if the new
973 # user data has glob patterns though.
976 raise "Can't change the username to #{newuser}" if defined?(@user) and newuser.has_irc_glob?
980 # We have to allow changing the host of an Irc User due to some networks
981 # (e.g. Freenode) changing hostmasks on the fly. We still check if the new
982 # host data has glob patterns though.
985 raise "Can't change the hostname to #{newhost}" if defined?(@host) and newhost.has_irc_glob?
989 # Checks if a User is well-known or not by looking at the hostname and user
992 return nick != "*" && user != "*" && host != "*"
1001 # Set the away status of the user. Use away=(nil) or away=(false)
1012 # Since to_irc_user runs the same checks on server and channel as
1013 # to_irc_netmask, we just try that and return self if it works.
1015 # Subclasses of User will return self if possible.
1017 def to_irc_user(opts={})
1018 return self if fits_with_server_and_casemap?(opts)
1019 return self.full_downcase.to_irc_user(opts)
1022 # We can replace everything at once with data from another User
1027 self.nick = other.nick
1028 self.user = other.user
1029 self.host = other.host
1030 @server = other.server
1031 @casemap = other.casemap unless @server
1034 self.replace(other.to_irc_user(server_and_casemap))
1038 def modes_on(channel)
1041 channel.modes_of(self)
1043 return @server.channel(channel).modes_of(self) if @server
1044 raise "Can't resolve channel #{channel}"
1051 channel.has_op?(self)
1053 return @server.channel(channel).has_op?(self) if @server
1054 raise "Can't resolve channel #{channel}"
1058 def is_voice?(channel)
1061 channel.has_voice?(self)
1063 return @server.channel(channel).has_voice?(self) if @server
1064 raise "Can't resolve channel #{channel}"
1070 @server.channels.select { |ch| ch.has_user?(self) }
1078 # A UserList is an ArrayOf <code>User</code>s
1079 # We derive it from NetmaskList, which allows us to inherit any special
1080 # NetmaskList method
1082 class UserList < NetmaskList
1084 # Create a new UserList, optionally filling it with the elements from
1085 # the Array argument fed to it.
1087 def initialize(ar=[])
1089 @element_class = User
1092 # Convenience method: convert the UserList to a list of nicks. The indices
1096 self.map { |user| user.nick }
1105 # We keep extending String, this time adding a method that converts a
1106 # String into an Irc::User object
1108 def to_irc_user(opts={})
1109 Irc::User.new(self, opts)
1116 # An IRC Channel is identified by its name, and it has a set of properties:
1117 # * a Channel::Topic
1119 # * a set of Channel::Modes
1121 # The Channel::Topic and Channel::Mode classes are defined within the
1122 # Channel namespace because they only make sense there
1130 attr_reader :channel
1137 # Hash of modes. Subclass of Hash that defines any? and all?
1138 # to check if boolean modes (Type D) are set
1139 class ModeHash < Hash
1141 !!ar.find { |m| s = m.to_sym ; self[s] && self[s].set? }
1144 !ar.find { |m| s = m.to_sym ; !(self[s] && self[s].set?) }
1148 # Channel modes of type A manipulate lists
1150 # Example: b (banlist)
1152 class ModeTypeA < Mode
1156 @list = NetmaskList.new
1160 nm = @channel.server.new_netmask(val)
1161 @list << nm unless @list.include?(nm)
1165 nm = @channel.server.new_netmask(val)
1172 # Channel modes of type B need an argument
1176 class ModeTypeB < Mode
1185 alias :value :status
1192 @arg = nil if @arg == val
1198 # Channel modes that change the User prefixes are like
1199 # Channel modes of type B, except that they manipulate
1200 # lists of Users, so they are somewhat similar to channel
1203 class UserMode < ModeTypeB
1208 @list = UserList.new
1212 u = @channel.server.user(val)
1213 @list << u unless @list.include?(u)
1217 u = @channel.server.user(val)
1224 # Channel modes of type C need an argument when set,
1225 # but not when they get reset
1227 # Example: l (limit)
1229 class ModeTypeC < Mode
1238 alias :value :status
1251 # Channel modes of type D are basically booleans
1253 # Example: m (moderate)
1255 class ModeTypeD < Mode
1276 # A Topic represents the topic of a channel. It consists of
1277 # the topic itself, who set it and when
1280 attr_accessor :text, :set_by, :set_on
1283 # Create a new Topic setting the text, the creator and
1286 def initialize(text="", set_by="", set_on=Time.new)
1288 @set_by = set_by.to_irc_netmask
1292 # Replace a Topic with another one
1295 raise TypeError, "#{topic.inspect} is not of class #{self.class}" unless topic.kind_of?(self.class)
1296 @text = topic.text.dup
1297 @set_by = topic.set_by.dup
1298 @set_on = topic.set_on.dup
1303 def to_irc_channel_topic
1316 # Returns an Irc::Channel::Topic with self as text
1318 def to_irc_channel_topic
1319 Irc::Channel::Topic.new(self)
1328 # Here we start with the actual Channel class
1332 # Return the non-prefixed part of a channel name.
1333 # Also works with ## channels found on some networks
1335 def self.npname(str)
1336 return str.to_s.sub(/^[&#+!]+/,'')
1339 include ServerOrCasemap
1340 attr_reader :name, :topic, :mode, :users
1342 attr_accessor :creation_time, :url
1345 str = self.__to_s__[0..-2]
1346 str << " on server #{server}" if server
1347 str << " @name=#{@name.inspect} @topic=#{@topic.text.inspect}"
1348 str << " @users=[#{user_nicks.sort.join(', ')}]"
1349 str << " (created on #{creation_time})" if creation_time
1350 str << " (URL #{url})" if url
1362 @users.map { |u| u.downcase }
1365 # Checks if the receiver already has a user with the given _nick_
1368 @users.index(nick.to_irc_user(server_and_casemap))
1371 # Returns the user with nick _nick_, if available
1374 idx = has_user?(nick)
1378 # Adds a user to the channel
1380 def add_user(user, opts={})
1381 silent = opts.fetch(:silent, false)
1383 warn "Trying to add user #{user} to channel #{self} again" unless silent
1385 @users << user.to_irc_user(server_and_casemap)
1389 # Creates a new channel with the given name, optionally setting the topic
1390 # and an initial users list.
1392 # No additional info is created here, because the channel flags and userlists
1393 # allowed depend on the server.
1395 def initialize(name, topic=nil, users=[], opts={})
1396 raise ArgumentError, "Channel name cannot be empty" if name.to_s.empty?
1397 warn "Unknown channel prefix #{name[0,1]}" if name !~ /^[&#+!]/
1398 raise ArgumentError, "Invalid character in #{name.inspect}" if name =~ /[ \x07,]/
1400 init_server_or_casemap(opts)
1404 @topic = topic ? topic.to_irc_channel_topic : Channel::Topic.new
1406 @users = UserList.new
1413 @mode = ModeHash.new
1415 # creation time, only on some networks
1416 @creation_time = nil
1418 # URL, only on some networks
1422 # Removes a user from the channel
1424 def delete_user(user)
1425 @mode.each { |sym, mode|
1426 mode.reset(user) if mode.kind_of?(UserMode)
1431 # The channel prefix
1437 # A channel is local to a server if it has the '&' prefix
1443 # A channel is modeless if it has the '+' prefix
1449 # A channel is safe if it has the '!' prefix
1455 # A channel is normal if it has the '#' prefix
1463 def create_mode(sym, kl)
1464 @mode[sym.to_sym] = kl.new(self)
1470 l << s if (m.class <= UserMode and m.list[user])
1476 @mode.has_key?(:o) and @mode[:o].list[user]
1479 def has_voice?(user)
1480 @mode.has_key?(:v) and @mode[:v].list[user]
1485 # A ChannelList is an ArrayOf <code>Channel</code>s
1487 class ChannelList < ArrayOf
1489 # Create a new ChannelList, optionally filling it with the elements from
1490 # the Array argument fed to it.
1492 def initialize(ar=[])
1496 # Convenience method: convert the ChannelList to a list of channel names.
1497 # The indices are preserved
1500 self.map { |chan| chan.name }
1510 # We keep extending String, this time adding a method that converts a
1511 # String into an Irc::Channel object
1513 def to_irc_channel(opts={})
1514 Irc::Channel.new(self, opts)
1523 # An IRC Server represents the Server the client is connected to.
1527 attr_reader :hostname, :version, :usermodes, :chanmodes
1528 attr_reader :supports, :capabilities
1530 attr_reader :channels, :users
1534 @channels.map { |ch| ch.downcase }
1539 @users.map { |u| u.downcase }
1543 chans, users = [@channels, @users].map {|d|
1545 a.downcase <=> b.downcase
1551 str = self.__to_s__[0..-2]
1552 str << " @hostname=#{hostname}"
1553 str << " @channels=#{chans}"
1554 str << " @users=#{users}"
1559 hostname.nil? ? "<no hostname>" : hostname
1562 # Create a new Server, with all instance variables reset to nil (for
1563 # scalar variables), empty channel and user lists and @supports
1564 # initialized to the default values for all known supported features.
1567 @hostname = @version = @usermodes = @chanmodes = nil
1569 @channels = ChannelList.new
1571 @users = UserList.new
1576 # Resets the server capabilities
1578 def reset_capabilities
1580 :casemapping => 'rfc1459'.to_irc_casemap,
1583 :typea => nil, # Type A: address lists
1584 :typeb => nil, # Type B: needs a parameter
1585 :typec => nil, # Type C: needs a parameter when set
1586 :typed => nil # Type D: must not have a parameter
1589 :chantypes => "#&!+",
1600 :prefixes => [:"@", :+]
1611 # Convert a mode (o, v, h, ...) to the corresponding
1612 # prefix (@, +, %, ...). See also mode_for_prefix
1613 def prefix_for_mode(mode)
1614 return @supports[:prefix][:prefixes][
1615 @supports[:prefix][:modes].index(mode.to_sym)
1619 # Convert a prefix (@, +, %, ...) to the corresponding
1620 # mode (o, v, h, ...). See also prefix_for_mode
1621 def mode_for_prefix(pfx)
1622 return @supports[:prefix][:modes][
1623 @supports[:prefix][:prefixes].index(pfx.to_sym)
1627 # Resets the Channel and User list
1630 @users.reverse_each { |u|
1633 @channels.reverse_each { |u|
1643 @hostname = @version = @usermodes = @chanmodes = nil
1646 # This method is used to parse a 004 RPL_MY_INFO line
1648 def parse_my_info(line)
1649 ar = line.split(' ')
1656 def noval_warn(key, val, &block)
1658 yield if block_given?
1660 warn "No #{key.to_s.upcase} value"
1664 def val_warn(key, val, &block)
1665 if val == true or val == false or val.nil?
1666 yield if block_given?
1668 warn "No #{key.to_s.upcase} value must be specified, got #{val}"
1671 private :noval_warn, :val_warn
1673 # This method is used to parse a 005 RPL_ISUPPORT line
1675 # See the RPL_ISUPPORT draft[http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt]
1677 def parse_isupport(line)
1678 debug "Parsing ISUPPORT #{line.inspect}"
1679 ar = line.split(' ')
1682 prekey, val = en.split('=', 2)
1683 if prekey =~ /^-(.*)/
1684 key = $1.downcase.to_sym
1687 key = prekey.downcase.to_sym
1691 noval_warn(key, val) {
1693 reparse << "CASEMAPPING=(charset)"
1695 # TODO some servers offer non-standard CASEMAPPINGs in the form
1696 # locale.charset[-options], which indicate an extended set of
1697 # allowed characters (mostly for nicks). This might be supported
1698 # with hooks for the unicode core module
1699 @supports[key] = val.to_irc_casemap
1702 when :chanlimit, :idchan, :maxlist, :targmax
1703 noval_warn(key, val) {
1704 groups = val.split(',')
1707 @supports[key][k] = v.to_i || 0
1708 if @supports[key][k] == 0
1709 warn "Deleting #{key} limit of 0 for #{k}"
1710 @supports[key].delete(k)
1715 noval_warn(key, val) {
1716 groups = val.split(',')
1717 @supports[key][:typea] = groups[0].scan(/./).map { |x| x.to_sym}
1718 @supports[key][:typeb] = groups[1].scan(/./).map { |x| x.to_sym}
1719 @supports[key][:typec] = groups[2].scan(/./).map { |x| x.to_sym}
1720 @supports[key][:typed] = groups[3].scan(/./).map { |x| x.to_sym}
1722 when :channellen, :kicklen, :modes, :topiclen
1724 @supports[key] = val.to_i
1726 @supports[key] = nil
1729 @supports[key] = val # can also be nil
1732 @supports[key] = val
1735 @supports[key] = val
1737 noval_warn(key, val) {
1738 reparse << "CHANLIMIT=(chantypes):#{val} "
1741 noval_warn(key, val) {
1742 @supports[:targmax]['PRIVMSG'] = val.to_i
1743 @supports[:targmax]['NOTICE'] = val.to_i
1746 noval_warn(key, val) {
1747 @supports[key] = val
1750 noval_warn(key, val) {
1751 @supports[key] = val.to_i
1755 val.scan(/\((.*)\)(.*)/) { |m, p|
1756 @supports[key][:modes] = m.scan(/./).map { |x| x.to_sym}
1757 @supports[key][:prefixes] = p.scan(/./).map { |x| x.to_sym}
1760 @supports[key][:modes] = nil
1761 @supports[key][:prefixes] = nil
1764 val_warn(key, val) {
1765 @supports[key] = val.nil? ? true : val
1768 noval_warn(key, val) {
1769 @supports[key] = val.scan(/./)
1772 noval_warn(key, val) {
1773 @supports[key] = val.split(',')
1776 @supports[key] = val.nil? ? true : val
1779 unless reparse.empty?
1780 reparse_str = reparse.join(" ")
1781 reparse_str.gsub!("(chantypes)",@supports[:chantypes])
1782 reparse_str.gsub!("(charset)",@supports[:charset] || 'rfc1459')
1783 parse_isupport(reparse_str)
1787 # Returns the casemap of the server.
1790 @supports[:casemapping]
1793 # Returns User or Channel depending on what _name_ can be
1796 def user_or_channel?(name)
1797 if supports[:chantypes].include?(name[0])
1804 # Returns the actual User or Channel object matching _name_
1806 def user_or_channel(name)
1807 if supports[:chantypes].include?(name[0])
1808 return channel(name)
1814 # Checks if the receiver already has a channel with the given _name_
1816 def has_channel?(name)
1817 return false if name.nil_or_empty?
1818 channel_names.index(name.irc_downcase(casemap))
1820 alias :has_chan? :has_channel?
1822 # Returns the channel with name _name_, if available
1824 def get_channel(name)
1825 return nil if name.nil_or_empty?
1826 idx = has_channel?(name)
1827 channels[idx] if idx
1829 alias :get_chan :get_channel
1831 # Create a new Channel object bound to the receiver and add it to the
1832 # list of <code>Channel</code>s on the receiver, unless the channel was
1833 # present already. In this case, the default action is to raise an
1834 # exception, unless _fails_ is set to false. An exception can also be
1835 # raised if _str_ is nil or empty, again only if _fails_ is set to true;
1836 # otherwise, the method just returns nil
1838 def new_channel(name, topic=nil, users=[], fails=true)
1839 if name.nil_or_empty?
1840 raise "Tried to look for empty or nil channel name #{name.inspect}" if fails
1845 raise "Channel #{name} already exists on server #{self}" if fails
1851 # Give a warning if the new Channel goes over some server limits.
1853 # FIXME might need to raise an exception
1855 warn "#{self} doesn't support channel prefix #{prefix}" unless @supports[:chantypes].include?(prefix)
1856 warn "#{self} doesn't support channel names this long (#{name.length} > #{@supports[:channellen]})" unless name.length <= @supports[:channellen]
1858 # Next, we check if we hit the limit for channels of type +prefix+
1859 # if the server supports +chanlimit+
1861 @supports[:chanlimit].keys.each { |k|
1862 next unless k.include?(prefix)
1864 channel_names.each { |n|
1865 count += 1 if k.include?(n[0])
1867 # raise IndexError, "Already joined #{count} channels with prefix #{k}" if count == @supports[:chanlimit][k]
1868 warn "Already joined #{count}/#{@supports[:chanlimit][k]} channels with prefix #{k}, we may be going over server limits" if count >= @supports[:chanlimit][k]
1871 # So far, everything is fine. Now create the actual Channel
1873 chan = Channel.new(name, topic, users, :server => self)
1875 # We wade through +prefix+ and +chanmodes+ to create appropriate
1876 # lists and flags for this channel
1878 @supports[:prefix][:modes].each { |mode|
1879 chan.create_mode(mode, Channel::UserMode)
1880 } if @supports[:prefix][:modes]
1882 @supports[:chanmodes].each { |k, val|
1887 chan.create_mode(mode, Channel::ModeTypeA)
1891 chan.create_mode(mode, Channel::ModeTypeB)
1895 chan.create_mode(mode, Channel::ModeTypeC)
1899 chan.create_mode(mode, Channel::ModeTypeD)
1906 # debug "Created channel #{chan.inspect}"
1911 # Returns the Channel with the given _name_ on the server,
1912 # creating it if necessary. This is a short form for
1913 # new_channel(_str_, nil, [], +false+)
1916 new_channel(str,nil,[],false)
1919 # Remove Channel _name_ from the list of <code>Channel</code>s
1921 def delete_channel(name)
1922 idx = has_channel?(name)
1923 raise "Tried to remove unmanaged channel #{name}" unless idx
1924 @channels.delete_at(idx)
1927 # Checks if the receiver already has a user with the given _nick_
1930 return false if nick.nil_or_empty?
1931 user_nicks.index(nick.irc_downcase(casemap))
1934 # Returns the user with nick _nick_, if available
1937 idx = has_user?(nick)
1941 # Create a new User object bound to the receiver and add it to the list
1942 # of <code>User</code>s on the receiver, unless the User was present
1943 # already. In this case, the default action is to raise an exception,
1944 # unless _fails_ is set to false. An exception can also be raised
1945 # if _str_ is nil or empty, again only if _fails_ is set to true;
1946 # otherwise, the method just returns nil
1948 def new_user(str, fails=true)
1949 if str.nil_or_empty?
1950 raise "Tried to look for empty or nil user name #{str.inspect}" if fails
1953 tmp = str.to_irc_user(:server => self)
1954 old = get_user(tmp.nick)
1955 # debug "Tmp: #{tmp.inspect}"
1956 # debug "Old: #{old.inspect}"
1958 # debug "User already existed as #{old.inspect}"
1961 # debug "Both were known"
1962 # Do not raise an error: things like Freenode change the hostname after identification
1963 warning "User #{tmp.nick} has inconsistent Netmasks! #{self} knows #{old.inspect} but access was tried with #{tmp.inspect}" if old != tmp
1964 raise "User #{tmp} already exists on server #{self}" if fails
1966 if old.fullform.downcase != tmp.fullform.downcase
1968 # debug "Known user now #{old.inspect}"
1973 warn "#{self} doesn't support nicknames this long (#{tmp.nick.length} > #{@supports[:nicklen]})" unless tmp.nick.length <= @supports[:nicklen]
1979 # Returns the User with the given Netmask on the server,
1980 # creating it if necessary. This is a short form for
1981 # new_user(_str_, +false+)
1984 new_user(str, false)
1987 # Deletes User _user_ from Channel _channel_
1989 def delete_user_from_channel(user, channel)
1990 channel.delete_user(user)
1993 # Remove User _someuser_ from the list of <code>User</code>s.
1994 # _someuser_ must be specified with the full Netmask.
1996 def delete_user(someuser)
1997 idx = has_user?(someuser)
1998 raise "Tried to remove unmanaged user #{user}" unless idx
1999 have = self.user(someuser)
2000 @channels.each { |ch|
2001 delete_user_from_channel(have, ch)
2003 @users.delete_at(idx)
2006 # Create a new Netmask object with the appropriate casemap
2008 def new_netmask(str)
2009 str.to_irc_netmask(:server => self)
2012 # Finds all <code>User</code>s on server whose Netmask matches _mask_
2014 def find_users(mask)
2015 nm = new_netmask(mask)
2016 @users.inject(UserList.new) {
2018 if user.user == "*" or user.host == "*"
2019 list << user if user.nick.irc_downcase(casemap) =~ nm.nick.irc_downcase(casemap).to_irc_regexp
2021 list << user if user.matches?(nm)