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 remove him from the Server @users list
\r
12 # This module defines the fundamental building blocks for IRC
\r
14 # Author:: Giuseppe Bilotta (giuseppe.bilotta@gmail.com)
\r
15 # Copyright:: Copyright (c) 2006 Giuseppe Bilotta
\r
18 # TODO User should have associated Server too
\r
20 # TODO rather than the complex init methods, we should provide a single one (having a String parameter)
\r
21 # and then provide to_irc_netmask(casemap), to_irc_user(server), to_irc_channel(server) etc
\r
24 # We start by extending the String class
\r
25 # with some IRC-specific methods
\r
29 # This method returns a string which is the downcased version of the
\r
30 # receiver, according to IRC rules: due to the Scandinavian origin of IRC,
\r
31 # the characters <tt>{}|^</tt> are considered the uppercase equivalent of
\r
34 # Since IRC is mostly case-insensitive (the Windows way: case is preserved,
\r
35 # but it's actually ignored to check equality), this method is rather
\r
36 # important when checking if two strings refer to the same entity
\r
39 # Modern server allow different casemaps, too, in which some or all
\r
40 # of the extra characters are not converted
\r
42 def irc_downcase(casemap='rfc1459')
\r
45 self.tr("\x41-\x5e", "\x61-\x7e")
\r
46 when 'strict-rfc1459'
\r
47 self.tr("\x41-\x5d", "\x61-\x7d")
\r
49 self.tr("\x41-\x5a", "\x61-\x7a")
\r
51 raise TypeError, "Unknown casemap #{casemap}"
\r
55 # This is the same as the above, except that the string is altered in place
\r
57 # See also the discussion about irc_downcase
\r
59 def irc_downcase!(casemap='rfc1459')
\r
62 self.tr!("\x41-\x5e", "\x61-\x7e")
\r
63 when 'strict-rfc1459'
\r
64 self.tr!("\x41-\x5d", "\x61-\x7d")
\r
66 self.tr!("\x41-\x5a", "\x61-\x7a")
\r
68 raise TypeError, "Unknown casemap #{casemap}"
\r
72 # Upcasing functions are provided too
\r
74 # See also the discussion about irc_downcase
\r
76 def irc_upcase(casemap='rfc1459')
\r
79 self.tr("\x61-\x7e", "\x41-\x5e")
\r
80 when 'strict-rfc1459'
\r
81 self.tr("\x61-\x7d", "\x41-\x5d")
\r
83 self.tr("\x61-\x7a", "\x41-\x5a")
\r
85 raise TypeError, "Unknown casemap #{casemap}"
\r
91 # See also the discussion about irc_downcase
\r
93 def irc_upcase!(casemap='rfc1459')
\r
96 self.tr!("\x61-\x7e", "\x41-\x5e")
\r
97 when 'strict-rfc1459'
\r
98 self.tr!("\x61-\x7d", "\x41-\x5d")
\r
100 self.tr!("\x61-\x7a", "\x41-\x5a")
\r
102 raise TypeError, "Unknown casemap #{casemap}"
\r
106 # This method checks if the receiver contains IRC glob characters
\r
108 # IRC has a very primitive concept of globs: a <tt>*</tt> stands for "any
\r
109 # number of arbitrary characters", a <tt>?</tt> stands for "one and exactly
\r
110 # one arbitrary character". These characters can be escaped by prefixing them
\r
111 # with a slash (<tt>\\</tt>).
\r
113 # A known limitation of this glob syntax is that there is no way to escape
\r
114 # the escape character itself, so it's not possible to build a glob pattern
\r
115 # where the escape character precedes a glob.
\r
118 self =~ /^[*?]|[^\\][*?]/
\r
121 # This method is used to convert the receiver into a Regular Expression
\r
122 # that matches according to the IRC glob syntax
\r
125 regmask = Regexp.escape(self)
\r
126 regmask.gsub!(/(\\\\)?\\[*?]/) { |m|
\r
135 raise "Unexpected match #{m} when converting #{self}"
\r
138 Regexp.new(regmask)
\r
143 # ArrayOf is a subclass of Array whose elements are supposed to be all
\r
144 # of the same class. This is not intended to be used directly, but rather
\r
145 # to be subclassed as needed (see for example Irc::UserList and Irc::NetmaskList)
\r
147 # Presently, only very few selected methods from Array are overloaded to check
\r
148 # if the new elements are the correct class. An orthodox? method is provided
\r
149 # to check the entire ArrayOf against the appropriate class.
\r
151 class ArrayOf < Array
\r
153 attr_reader :element_class
\r
155 # Create a new ArrayOf whose elements are supposed to be all of type _kl_,
\r
156 # optionally filling it with the elements from the Array argument.
\r
158 def initialize(kl, ar=[])
\r
159 raise TypeError, "#{kl.inspect} must be a class name" unless kl.kind_of?(Class)
\r
161 @element_class = kl
\r
166 raise TypeError, "#{self.class} can only be initialized from an Array"
\r
171 "#<#{self.class}[#{@element_class}]:#{'0x%x' % self.object_id}: #{super}>"
\r
174 # Private method to check the validity of the elements passed to it
\r
175 # and optionally raise an error
\r
177 # TODO should it accept nils as valid?
\r
179 def internal_will_accept?(raising, *els)
\r
181 unless el.kind_of?(@element_class)
\r
182 raise TypeError, "#{el.inspect} is not of class #{@element_class}" if raising
\r
188 private :internal_will_accept?
\r
190 # This method checks if the passed arguments are acceptable for our ArrayOf
\r
192 def will_accept?(*els)
\r
193 internal_will_accept?(false, *els)
\r
196 # This method checks that all elements are of the appropriate class
\r
199 will_accept?(*self)
\r
202 # This method is similar to the above, except that it raises an exception
\r
203 # if the receiver is not valid
\r
205 raise TypeError unless valid?
\r
208 # Overloaded from Array#<<, checks for appropriate class of argument
\r
211 super(el) if internal_will_accept?(true, el)
\r
214 # Overloaded from Array#&, checks for appropriate class of argument elements
\r
218 ArrayOf.new(@element_class, r) if internal_will_accept?(true, *r)
\r
221 # Overloaded from Array#+, checks for appropriate class of argument elements
\r
224 ArrayOf.new(@element_class, super(ar)) if internal_will_accept?(true, *ar)
\r
227 # Overloaded from Array#-, so that an ArrayOf is returned. There is no need
\r
228 # to check the validity of the elements in the argument
\r
231 ArrayOf.new(@element_class, super(ar)) # if internal_will_accept?(true, *ar)
\r
234 # Overloaded from Array#|, checks for appropriate class of argument elements
\r
237 ArrayOf.new(@element_class, super(ar)) if internal_will_accept?(true, *ar)
\r
240 # Overloaded from Array#concat, checks for appropriate class of argument
\r
244 super(ar) if internal_will_accept?(true, *ar)
\r
247 # Overloaded from Array#insert, checks for appropriate class of argument
\r
250 def insert(idx, *ar)
\r
251 super(idx, *ar) if internal_will_accept?(true, *ar)
\r
254 # Overloaded from Array#replace, checks for appropriate class of argument
\r
258 super(ar) if (ar.kind_of?(ArrayOf) && ar.element_class <= @element_class) or internal_will_accept?(true, *ar)
\r
261 # Overloaded from Array#push, checks for appropriate class of argument
\r
265 super(*ar) if internal_will_accept?(true, *ar)
\r
268 # Overloaded from Array#unshift, checks for appropriate class of argument(s)
\r
272 super(el) if internal_will_accept?(true, *els)
\r
276 # Modifying methods which we don't handle yet are made private
\r
278 private :[]=, :collect!, :map!, :fill, :flatten!
\r
283 # The Irc module is used to keep all IRC-related classes
\r
284 # in the same namespace
\r
289 # A Netmask identifies each user by collecting its nick, username and
\r
290 # hostname in the form <tt>nick!user@host</tt>
\r
292 # Netmasks can also contain glob patterns in any of their components; in this
\r
293 # form they are used to refer to more than a user or to a user appearing
\r
298 # * <tt>*!*@*</tt> refers to everybody
\r
299 # * <tt>*!someuser@somehost</tt> refers to user +someuser+ on host +somehost+
\r
300 # regardless of the nick used.
\r
303 attr_reader :nick, :user, :host
\r
304 attr_reader :casemap
\r
307 # Netmask.new(netmask) => new_netmask
\r
308 # Netmask.new(hash={}, casemap=nil) => new_netmask
\r
309 # Netmask.new("nick!user@host", casemap=nil) => new_netmask
\r
311 # Create a new Netmask in any of these forms
\r
312 # 1. from another Netmask (does a .dup)
\r
313 # 2. from a Hash with any of the keys <tt>:nick</tt>, <tt>:user</tt> and
\r
315 # 3. from a String in the form <tt>nick!user@host</tt>
\r
317 # In all but the first forms a casemap may be speficied, the default
\r
320 # The nick is downcased following IRC rules and according to the given casemap.
\r
322 # FIXME check if user and host need to be downcased too.
\r
324 # Empty +nick+, +user+ or +host+ are converted to the generic glob pattern
\r
326 def initialize(str={}, casemap=nil)
\r
329 raise ArgumentError, "Can't set casemap when initializing from other Netmask" if casemap
\r
330 @casemap = str.casemap.dup
\r
331 @nick = str.nick.dup
\r
332 @user = str.user.dup
\r
333 @host = str.host.dup
\r
335 @casemap = casemap || str[:casemap] || 'rfc1459'
\r
336 @nick = str[:nick].to_s.irc_downcase(@casemap)
\r
337 @user = str[:user].to_s
\r
338 @host = str[:host].to_s
\r
342 @casemap = casemap || 'rfc1459'
\r
346 when /^(\S+?)(?:!(\S+)@(?:(\S+))?)?$/
\r
347 @casemap = casemap || 'rfc1459'
\r
348 @nick = $1.irc_downcase(@casemap)
\r
352 raise ArgumentError, "#{str} is not a valid netmask"
\r
355 raise ArgumentError, "#{str} is not a valid netmask"
\r
358 @nick = "*" if @nick.to_s.empty?
\r
359 @user = "*" if @user.to_s.empty?
\r
360 @host = "*" if @host.to_s.empty?
\r
364 str = "<#{self.class}:#{'0x%x' % self.object_id}:"
\r
365 str << " @nick=#{@nick.inspect} @user=#{@user.inspect}"
\r
366 str << " @host=#{@host.inspect}>"
\r
370 # Equality: two Netmasks are equal if they have the same @nick, @user, @host and @casemap
\r
373 self.class == other.class && @nick == other.nick && @user == other.user && @host == other.host && @casemap == other.casemap
\r
376 # This method changes the nick of the Netmask, downcasing the argument
\r
377 # following IRC rules and defaulting to the generic glob pattern if
\r
378 # the result is the null string.
\r
381 @nick = newnick.to_s.irc_downcase(@casemap)
\r
382 @nick = "*" if @nick.empty?
\r
385 # This method changes the user of the Netmask, defaulting to the generic
\r
386 # glob pattern if the result is the null string.
\r
389 @user = newuser.to_s
\r
390 @user = "*" if @user.empty?
\r
393 # This method changes the hostname of the Netmask, defaulting to the generic
\r
394 # glob pattern if the result is the null string.
\r
397 @host = newhost.to_s
\r
398 @host = "*" if @host.empty?
\r
401 # This method changes the casemap of a Netmask, which is needed in some
\r
402 # extreme circumstances. Please use sparingly
\r
404 def casemap=(newcmap)
\r
405 @casemap = newcmap.to_s
\r
406 @casemap = "rfc1459" if @casemap.empty?
\r
409 # This method checks if a Netmask is definite or not, by seeing if
\r
410 # any of its components are defined by globs
\r
413 return @nick.has_irc_glob? || @user.has_irc_glob? || @host.has_irc_glob?
\r
416 # A Netmask is easily converted to a String for the usual representation
\r
419 return "#{nick}!#{user}@#{host}"
\r
421 alias :to_s :fullform
\r
423 # This method is used to match the current Netmask against another one
\r
425 # The method returns true if each component of the receiver matches the
\r
426 # corresponding component of the argument. By _matching_ here we mean that
\r
427 # any netmask described by the receiver is also described by the argument.
\r
429 # In this sense, matching is rather simple to define in the case when the
\r
430 # receiver has no globs: it is just necessary to check if the argument
\r
431 # describes the receiver, which can be done by matching it against the
\r
432 # argument converted into an IRC Regexp (see String#to_irc_regexp).
\r
434 # The situation is also easy when the receiver has globs and the argument
\r
435 # doesn't, since in this case the result is false.
\r
437 # The more complex case in which both the receiver and the argument have
\r
438 # globs is not handled yet.
\r
441 cmp = Netmask.new(arg)
\r
442 raise TypeError, "#{arg} and #{self} have different casemaps" if @casemap != cmp.casemap
\r
443 raise TypeError, "#{arg} is not a valid Netmask" unless cmp.kind_of?(Netmask)
\r
444 [:nick, :user, :host].each { |component|
\r
445 us = self.send(component)
\r
446 them = cmp.send(component)
\r
447 raise NotImplementedError if us.has_irc_glob? && them.has_irc_glob?
\r
448 return false if us.has_irc_glob? && !them.has_irc_glob?
\r
449 return false unless us =~ them.to_irc_regexp
\r
454 # Case equality. Checks if arg matches self
\r
457 Netmask.new(arg).matches?(self)
\r
463 self.fullform <=> arg.fullform
\r
465 self.to_s <=> arg.to_s
\r
472 # A NetmaskList is an ArrayOf <code>Netmask</code>s
\r
474 class NetmaskList < ArrayOf
\r
476 # Create a new NetmaskList, optionally filling it with the elements from
\r
477 # the Array argument fed to it.
\r
478 def initialize(ar=[])
\r
484 # An IRC User is identified by his/her Netmask (which must not have
\r
485 # globs). In fact, User is just a subclass of Netmask. However,
\r
486 # a User will not allow one's host or user data to be changed.
\r
488 # Due to the idiosincrasies of the IRC protocol, we allow
\r
489 # the creation of a user with an unknown mask represented by the
\r
490 # glob pattern *@*. Only in this case they may be set.
\r
493 # * see if it's worth to add the other USER data
\r
494 # * see if it's worth to add NICKSERV status
\r
496 class User < Netmask
\r
499 # Create a new IRC User from a given Netmask (or anything that can be converted
\r
500 # into a Netmask) provided that the given Netmask does not have globs.
\r
502 def initialize(str="", casemap=nil)
\r
504 raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if nick.has_irc_glob? && nick != "*"
\r
505 raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if user.has_irc_glob? && user != "*"
\r
506 raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if host.has_irc_glob? && host != "*"
\r
510 # We only allow the user to be changed if it was "*". Otherwise,
\r
511 # we raise an exception if the new host is different from the old one
\r
517 raise "Can't change the username of user #{self}" if user != newuser
\r
521 # We only allow the host to be changed if it was "*". Otherwise,
\r
522 # we raise an exception if the new host is different from the old one
\r
528 raise "Can't change the hostname of user #{self}" if host != newhost
\r
532 # Checks if a User is well-known or not by looking at the hostname and user
\r
535 return user!="*" && host!="*"
\r
538 # Is the user away?
\r
544 # Set the away status of the user. Use away=(nil) or away=(false)
\r
557 # A UserList is an ArrayOf <code>User</code>s
\r
559 class UserList < ArrayOf
\r
561 # Create a new UserList, optionally filling it with the elements from
\r
562 # the Array argument fed to it.
\r
563 def initialize(ar=[])
\r
569 # A ChannelTopic represents the topic of a channel. It consists of
\r
570 # the topic itself, who set it and when
\r
572 attr_accessor :text, :set_by, :set_on
\r
575 # Create a new ChannelTopic setting the text, the creator and
\r
576 # the creation time
\r
577 def initialize(text="", set_by="", set_on=Time.new)
\r
583 # Replace a ChannelTopic with another one
\r
585 raise TypeError, "#{topic.inspect} is not an Irc::ChannelTopic" unless topic.kind_of?(ChannelTopic)
\r
586 @text = topic.text.dup
\r
587 @set_by = topic.set_by.dup
\r
588 @set_on = topic.set_on.dup
\r
593 # Mode on a channel
\r
601 # Channel modes of type A manipulate lists
\r
603 class ChannelModeTypeA < ChannelMode
\r
606 @list = NetmaskList.new
\r
610 nm = @channel.server.new_netmask(val)
\r
611 @list << nm unless @list.include?(nm)
\r
615 nm = @channel.server.new_netmask(val)
\r
620 # Channel modes of type B need an argument
\r
622 class ChannelModeTypeB < ChannelMode
\r
633 @arg = nil if @arg == val
\r
637 # Channel modes that change the User prefixes are like
\r
638 # Channel modes of type B, except that they manipulate
\r
639 # lists of Users, so they are somewhat similar to channel
\r
642 class ChannelUserMode < ChannelModeTypeB
\r
645 @list = UserList.new
\r
649 u = @channel.server.user(val)
\r
650 @list << u unless @list.include?(u)
\r
654 u = @channel.server.user(val)
\r
659 # Channel modes of type C need an argument when set,
\r
660 # but not when they get reset
\r
662 class ChannelModeTypeC < ChannelMode
\r
677 # Channel modes of type D are basically booleans
\r
678 class ChannelModeTypeD < ChannelMode
\r
698 # An IRC Channel is identified by its name, and it has a set of properties:
\r
704 attr_reader :name, :topic, :mode, :users, :server
\r
708 str = "<#{self.class}:#{'0x%x' % self.object_id}:"
\r
709 str << " on server #{server}"
\r
710 str << " @name=#{@name.inspect} @topic=#{@topic.text.inspect}"
\r
711 str << " @users=<#{@users.sort.join(', ')}>"
\r
715 # Creates a new channel with the given name, optionally setting the topic
\r
716 # and an initial users list.
\r
718 # No additional info is created here, because the channel flags and userlists
\r
719 # allowed depend on the server.
\r
721 # FIXME doesn't check if users have the same casemap as the channel yet
\r
723 def initialize(server, name, topic=nil, users=[])
\r
724 raise TypeError, "First parameter must be an Irc::Server" unless server.kind_of?(Server)
\r
725 raise ArgumentError, "Channel name cannot be empty" if name.to_s.empty?
\r
726 raise ArgumentError, "Unknown channel prefix #{name[0].chr}" if name !~ /^[&#+!]/
\r
727 raise ArgumentError, "Invalid character in #{name.inspect}" if name =~ /[ \x07,]/
\r
731 @name = name.irc_downcase(casemap)
\r
733 @topic = topic || ChannelTopic.new
\r
739 @users = UserList.new(users)
\r
741 raise ArgumentError, "Invalid user list #{users.inspect}"
\r
748 # Returns the casemap of the originating server
\r
750 return @server.casemap
\r
753 # Removes a user from the channel
\r
755 def delete_user(user)
\r
756 @mode.each { |sym, mode|
\r
757 mode.reset(user) if mode.kind_of?(ChannelUserMode)
\r
759 @users.delete(user)
\r
762 # The channel prefix
\r
768 # A channel is local to a server if it has the '&' prefix
\r
774 # A channel is modeless if it has the '+' prefix
\r
780 # A channel is safe if it has the '!' prefix
\r
786 # A channel is safe if it has the '#' prefix
\r
792 # Create a new mode
\r
794 def create_mode(sym, kl)
\r
795 @mode[sym.to_sym] = kl.new(self)
\r
800 # A ChannelList is an ArrayOf <code>Channel</code>s
\r
802 class ChannelList < ArrayOf
\r
804 # Create a new ChannelList, optionally filling it with the elements from
\r
805 # the Array argument fed to it.
\r
806 def initialize(ar=[])
\r
812 # An IRC Server represents the Server the client is connected to.
\r
816 attr_reader :hostname, :version, :usermodes, :chanmodes
\r
817 alias :to_s :hostname
\r
818 attr_reader :supports, :capabilities
\r
820 attr_reader :channels, :users
\r
823 @channels.map { |ch| ch.name }
\r
827 @users.map { |u| u.nick }
\r
831 chans = @channels.map { |ch|
\r
834 users = @users.map { |u|
\r
838 str = "<#{self.class}:#{'0x%x' % self.object_id}:"
\r
839 str << " @channels=#{chans}"
\r
840 str << " @users=#{users}>"
\r
844 # Create a new Server, with all instance variables reset
\r
845 # to nil (for scalar variables), the channel and user lists
\r
846 # are empty, and @supports is initialized to the default values
\r
847 # for all known supported features.
\r
850 @hostname = @version = @usermodes = @chanmodes = nil
\r
852 @channels = ChannelList.new
\r
854 @users = UserList.new
\r
859 # Resets the server capabilities
\r
861 def reset_capabilities
\r
863 :casemapping => 'rfc1459',
\r
866 :typea => nil, # Type A: address lists
\r
867 :typeb => nil, # Type B: needs a parameter
\r
868 :typec => nil, # Type C: needs a parameter when set
\r
869 :typed => nil # Type D: must not have a parameter
\r
871 :channellen => 200,
\r
872 :chantypes => "#&",
\r
882 :modes => 'ov'.scan(/./),
\r
883 :prefixes => '@+'.scan(/./)
\r
894 # Resets the Channel and User list
\r
900 @channels.each { |u|
\r
905 # Clears the server
\r
912 # This method is used to parse a 004 RPL_MY_INFO line
\r
914 def parse_my_info(line)
\r
915 ar = line.split(' ')
\r
922 def noval_warn(key, val, &block)
\r
924 yield if block_given?
\r
926 warn "No #{key.to_s.upcase} value"
\r
930 def val_warn(key, val, &block)
\r
931 if val == true or val == false or val.nil?
\r
932 yield if block_given?
\r
934 warn "No #{key.to_s.upcase} value must be specified, got #{val}"
\r
937 private :noval_warn, :val_warn
\r
939 # This method is used to parse a 005 RPL_ISUPPORT line
\r
941 # See the RPL_ISUPPORT draft[http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt]
\r
943 def parse_isupport(line)
\r
944 debug "Parsing ISUPPORT #{line.inspect}"
\r
945 ar = line.split(' ')
\r
948 prekey, val = en.split('=', 2)
\r
949 if prekey =~ /^-(.*)/
\r
950 key = $1.downcase.to_sym
\r
953 key = prekey.downcase.to_sym
\r
956 when :casemapping, :network
\r
957 noval_warn(key, val) {
\r
958 @supports[key] = val
\r
960 debug "Resetting casemap of #{u} from #{u.casemap} to #{val}"
\r
964 when :chanlimit, :idchan, :maxlist, :targmax
\r
965 noval_warn(key, val) {
\r
966 groups = val.split(',')
\r
968 k, v = g.split(':')
\r
969 @supports[key][k] = v.to_i
\r
973 noval_warn(key, val) {
\r
974 reparse += "CHANLIMIT=(chantypes):#{val} "
\r
977 noval_warn(key, val) {
\r
978 @supports[key]['PRIVMSG'] = val.to_i
\r
979 @supports[key]['NOTICE'] = val.to_i
\r
982 noval_warn(key, val) {
\r
983 groups = val.split(',')
\r
984 @supports[key][:typea] = groups[0].scan(/./).map { |x| x.to_sym}
\r
985 @supports[key][:typeb] = groups[1].scan(/./).map { |x| x.to_sym}
\r
986 @supports[key][:typec] = groups[2].scan(/./).map { |x| x.to_sym}
\r
987 @supports[key][:typed] = groups[3].scan(/./).map { |x| x.to_sym}
\r
989 when :channellen, :kicklen, :modes, :topiclen
\r
991 @supports[key] = val.to_i
\r
993 @supports[key] = nil
\r
996 @supports[key] = val # can also be nil
\r
999 @supports[key] = val
\r
1002 @supports[key] = val
\r
1004 noval_warn(key, val) {
\r
1005 @supports[key] = val.to_i
\r
1009 val.scan(/\((.*)\)(.*)/) { |m, p|
\r
1010 @supports[key][:modes] = m.scan(/./).map { |x| x.to_sym}
\r
1011 @supports[key][:prefixes] = p.scan(/./).map { |x| x.to_sym}
\r
1014 @supports[key][:modes] = nil
\r
1015 @supports[key][:prefixes] = nil
\r
1018 val_warn(key, val) {
\r
1019 @supports[key] = val.nil? ? true : val
\r
1022 noval_warn(key, val) {
\r
1023 @supports[key] = val.scan(/./)
\r
1026 noval_warn(key, val) {
\r
1027 @supports[key] = val.split(',')
\r
1030 @supports[key] = val.nil? ? true : val
\r
1033 reparse.gsub!("(chantypes)",@supports[:chantypes])
\r
1034 parse_isupport(reparse) unless reparse.empty?
\r
1037 # Returns the casemap of the server.
\r
1040 @supports[:casemapping] || 'rfc1459'
\r
1043 # Returns User or Channel depending on what _name_ can be
\r
1046 def user_or_channel?(name)
\r
1047 if supports[:chantypes].include?(name[0])
\r
1054 # Returns the actual User or Channel object matching _name_
\r
1056 def user_or_channel(name)
\r
1057 if supports[:chantypes].include?(name[0])
\r
1058 return channel(name)
\r
1064 # Checks if the receiver already has a channel with the given _name_
\r
1066 def has_channel?(name)
\r
1067 channel_names.index(name.to_s)
\r
1069 alias :has_chan? :has_channel?
\r
1071 # Returns the channel with name _name_, if available
\r
1073 def get_channel(name)
\r
1074 idx = channel_names.index(name.to_s)
\r
1075 channels[idx] if idx
\r
1077 alias :get_chan :get_channel
\r
1079 # Create a new Channel object and add it to the list of
\r
1080 # <code>Channel</code>s on the receiver, unless the channel
\r
1081 # was present already. In this case, the default action is
\r
1082 # to raise an exception, unless _fails_ is set to false
\r
1084 # The Channel is automatically created with the appropriate casemap
\r
1086 def new_channel(name, topic=nil, users=[], fails=true)
\r
1087 ex = get_chan(name)
\r
1089 raise "Channel #{name} already exists on server #{self}" if fails
\r
1093 prefix = name[0].chr
\r
1095 # Give a warning if the new Channel goes over some server limits.
\r
1097 # FIXME might need to raise an exception
\r
1099 warn "#{self} doesn't support channel prefix #{prefix}" unless @supports[:chantypes].include?(prefix)
\r
1100 warn "#{self} doesn't support channel names this long (#{name.length} > #{@supports[:channellen]})" unless name.length <= @supports[:channellen]
\r
1102 # Next, we check if we hit the limit for channels of type +prefix+
\r
1103 # if the server supports +chanlimit+
\r
1105 @supports[:chanlimit].keys.each { |k|
\r
1106 next unless k.include?(prefix)
\r
1108 channel_names.each { |n|
\r
1109 count += 1 if k.include?(n[0])
\r
1111 raise IndexError, "Already joined #{count} channels with prefix #{k}" if count == @supports[:chanlimit][k]
\r
1114 # So far, everything is fine. Now create the actual Channel
\r
1116 chan = Channel.new(self, name, topic, users)
\r
1118 # We wade through +prefix+ and +chanmodes+ to create appropriate
\r
1119 # lists and flags for this channel
\r
1121 @supports[:prefix][:modes].each { |mode|
\r
1122 chan.create_mode(mode, ChannelUserMode)
\r
1123 } if @supports[:prefix][:modes]
\r
1125 @supports[:chanmodes].each { |k, val|
\r
1130 chan.create_mode(mode, ChannelModeTypeA)
\r
1134 chan.create_mode(mode, ChannelModeTypeB)
\r
1138 chan.create_mode(mode, ChannelModeTypeC)
\r
1142 chan.create_mode(mode, ChannelModeTypeD)
\r
1149 # debug "Created channel #{chan.inspect}"
\r
1154 # Returns the Channel with the given _name_ on the server,
\r
1155 # creating it if necessary. This is a short form for
\r
1156 # new_channel(_str_, nil, [], +false+)
\r
1159 new_channel(str,nil,[],false)
\r
1162 # Remove Channel _name_ from the list of <code>Channel</code>s
\r
1164 def delete_channel(name)
\r
1165 idx = has_channel?(name)
\r
1166 raise "Tried to remove unmanaged channel #{name}" unless idx
\r
1167 @channels.delete_at(idx)
\r
1170 # Checks if the receiver already has a user with the given _nick_
\r
1172 def has_user?(nick)
\r
1173 user_nicks.index(nick.to_s)
\r
1176 # Returns the user with nick _nick_, if available
\r
1178 def get_user(nick)
\r
1179 idx = user_nicks.index(nick.to_s)
\r
1180 @users[idx] if idx
\r
1183 # Create a new User object and add it to the list of
\r
1184 # <code>User</code>s on the receiver, unless the User
\r
1185 # was present already. In this case, the default action is
\r
1186 # to raise an exception, unless _fails_ is set to false
\r
1188 # The User is automatically created with the appropriate casemap
\r
1190 def new_user(str, fails=true)
\r
1195 tmp = User.new(str, self.casemap)
\r
1197 # debug "Creating or selecting user #{tmp.inspect} from #{str.inspect}"
\r
1198 old = get_user(tmp.nick)
\r
1200 # debug "User already existed as #{old.inspect}"
\r
1203 # Do not raise an error: things like Freenode change the hostname after identification
\r
1204 warning "User #{tmp.nick} has inconsistent Netmasks! #{self} knows #{old.inspect} but access was tried with #{tmp.inspect}" if old != tmp
\r
1205 raise "User #{tmp} already exists on server #{self}" if fails
\r
1208 old.user = tmp.user
\r
1209 old.host = tmp.host
\r
1210 # debug "User improved to #{old.inspect}"
\r
1215 warn "#{self} doesn't support nicknames this long (#{tmp.nick.length} > #{@supports[:nicklen]})" unless tmp.nick.length <= @supports[:nicklen]
\r
1217 return @users.last
\r
1221 # Returns the User with the given Netmask on the server,
\r
1222 # creating it if necessary. This is a short form for
\r
1223 # new_user(_str_, +false+)
\r
1226 u = new_user(str, false)
\r
1227 debug "Server user #{u.inspect} from #{str.inspect}"
\r
1231 # Remove User _someuser_ from the list of <code>User</code>s.
\r
1232 # _someuser_ must be specified with the full Netmask.
\r
1234 def delete_user(someuser)
\r
1235 idx = has_user?(someuser.nick)
\r
1236 raise "Tried to remove unmanaged user #{user}" unless idx
\r
1237 have = self.user(someuser)
\r
1238 raise "User #{someuser.nick} has inconsistent Netmasks! #{self} knows #{have} but access was tried with #{someuser}" if have != someuser && have.user != "*" && have.host != "*"
\r
1239 @channels.each { |ch|
\r
1240 delete_user_from_channel(have, ch)
\r
1242 @users.delete_at(idx)
\r
1245 # Create a new Netmask object with the appropriate casemap
\r
1247 def new_netmask(str)
\r
1248 if str.kind_of?(Netmask )
\r
1249 raise "Wrong casemap for Netmask #{str.inspect}" if str.casemap != self.casemap
\r
1252 Netmask.new(str, self.casemap)
\r
1255 # Finds all <code>User</code>s on server whose Netmask matches _mask_
\r
1257 def find_users(mask)
\r
1258 nm = new_netmask(mask)
\r
1259 @users.inject(UserList.new) {
\r
1261 if user.user == "*" or user.host == "*"
\r
1262 list << user if user.nick =~ nm.nick.to_irc_regexp
\r
1264 list << user if user.matches?(nm)
\r
1270 # Deletes User from Channel
\r
1272 def delete_user_from_channel(user, channel)
\r
1273 channel.delete_user(user)
\r