]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/irc.rb
5d3b01ec1a1139551867b507fafaaa20c62390c3
[user/henk/code/ruby/rbot.git] / lib / rbot / irc.rb
1 #-- vim:sw=2:et\r
2 # General TODO list\r
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
7 #++\r
8 # :title: IRC module\r
9 #\r
10 # Basic IRC stuff\r
11 #\r
12 # This module defines the fundamental building blocks for IRC\r
13 #\r
14 # Author:: Giuseppe Bilotta (giuseppe.bilotta@gmail.com)\r
15 # Copyright:: Copyright (c) 2006 Giuseppe Bilotta\r
16 # License:: GPLv2\r
17 \r
18 require 'singleton'\r
19 \r
20 \r
21 # The Irc module is used to keep all IRC-related classes\r
22 # in the same namespace\r
23 #\r
24 module Irc\r
25 \r
26 \r
27   # Due to its Scandinavian origins, IRC has strange case mappings, which\r
28   # consider the characters <tt>{}|^</tt> as the uppercase\r
29   # equivalents of # <tt>[]\~</tt>.\r
30   #\r
31   # This is however not the same on all IRC servers: some use standard ASCII\r
32   # casemapping, other do not consider <tt>^</tt> as the uppercase of\r
33   # <tt>~</tt>\r
34   #\r
35   class Casemap\r
36     @@casemaps = {}\r
37 \r
38     # Create a new casemap with name _name_, uppercase characters _upper_ and\r
39     # lowercase characters _lower_\r
40     #\r
41     def initialize(name, upper, lower)\r
42       @key = name.to_sym\r
43       raise "Casemap #{name.inspect} already exists!" if @@casemaps.has_key?(@key)\r
44       @@casemaps[@key] = {\r
45         :upper => upper,\r
46         :lower => lower,\r
47         :casemap => self\r
48       }\r
49     end\r
50 \r
51     # Returns the Casemap with the given name\r
52     #\r
53     def Casemap.get(name)\r
54       @@casemaps[name.to_sym][:casemap]\r
55     end\r
56 \r
57     # Retrieve the 'uppercase characters' of this Casemap\r
58     #\r
59     def upper\r
60       @@casemaps[@key][:upper]\r
61     end\r
62 \r
63     # Retrieve the 'lowercase characters' of this Casemap\r
64     #\r
65     def lower\r
66       @@casemaps[@key][:lower]\r
67     end\r
68 \r
69     # Return a Casemap based on the receiver\r
70     #\r
71     def to_irc_casemap\r
72       self\r
73     end\r
74 \r
75     # A Casemap is represented by its lower/upper mappings\r
76     #\r
77     def inspect\r
78       "#<#{self.class}:#{'0x%x'% self.object_id}: #{upper.inspect} ~(#{self})~ #{lower.inspect}>"\r
79     end\r
80 \r
81     # As a String we return our name\r
82     #\r
83     def to_s\r
84       @key.to_s\r
85     end\r
86 \r
87     # Raise an error if _arg_ and self are not the same Casemap\r
88     #\r
89     def must_be(arg)\r
90       other = arg.to_irc_casemap\r
91       raise "Casemap mismatch (#{self} != #{other})" unless self == other\r
92       return true\r
93     end\r
94 \r
95   end\r
96 \r
97   # The rfc1459 casemap\r
98   #\r
99   class RfcCasemap < Casemap\r
100     include Singleton\r
101 \r
102     def initialize\r
103       super('rfc1459', "\x41-\x5e", "\x61-\x7e")\r
104     end\r
105 \r
106   end\r
107   RfcCasemap.instance\r
108 \r
109   # The strict-rfc1459 Casemap\r
110   #\r
111   class StrictRfcCasemap < Casemap\r
112     include Singleton\r
113 \r
114     def initialize\r
115       super('strict-rfc1459', "\x41-\x5d", "\x61-\x7d")\r
116     end\r
117 \r
118   end\r
119   StrictRfcCasemap.instance\r
120 \r
121   # The ascii Casemap\r
122   #\r
123   class AsciiCasemap < Casemap\r
124     include Singleton\r
125 \r
126     def initialize\r
127       super('ascii', "\x41-\x5a", "\x61-\x7a")\r
128     end\r
129 \r
130   end\r
131   AsciiCasemap.instance\r
132 \r
133 \r
134   # This module is included by all classes that are either bound to a server\r
135   # or should have a casemap.\r
136   #\r
137   module ServerOrCasemap\r
138 \r
139     attr_reader :server\r
140 \r
141     # This method initializes the instance variables @server and @casemap\r
142     # according to the values of the hash keys :server and :casemap in _opts_\r
143     #\r
144     def init_server_or_casemap(opts={})\r
145       @server = opts.fetch(:server, nil)\r
146       raise TypeError, "#{@server} is not a valid Irc::Server" if @server and not @server.kind_of?(Server)\r
147 \r
148       @casemap = opts.fetch(:casemap, nil)\r
149       if @server\r
150         if @casemap\r
151           @server.casemap.must_be(@casemap)\r
152           @casemap = nil\r
153         end\r
154       else\r
155         @casemap = (@casemap || 'rfc1459').to_irc_casemap\r
156       end\r
157     end\r
158 \r
159     # This is an auxiliary method: it returns true if the receiver fits the\r
160     # server and casemap specified in _opts_, false otherwise.\r
161     #\r
162     def fits_with_server_and_casemap?(opts={})\r
163       srv = opts.fetch(:server, nil)\r
164       cmap = opts.fetch(:casemap, nil)\r
165       cmap = cmap.to_irc_casemap unless cmap.nil?\r
166 \r
167       if srv.nil?\r
168         return true if cmap.nil? or cmap == casemap\r
169       else\r
170         return true if srv == @server and (cmap.nil? or cmap == casemap)\r
171       end\r
172       return false\r
173     end\r
174 \r
175     # Returns the casemap of the receiver, by looking at the bound\r
176     # @server (if possible) or at the @casemap otherwise\r
177     #\r
178     def casemap\r
179       @server.casemap rescue @casemap\r
180     end\r
181 \r
182     # Returns a hash with the current @server and @casemap as values of\r
183     # :server and :casemap\r
184     #\r
185     def server_and_casemap\r
186       {:server => @server, :casemap => @casemap}\r
187     end\r
188 \r
189     # We allow up/downcasing with a different casemap\r
190     #\r
191     def irc_downcase(cmap=casemap)\r
192       self.to_s.irc_downcase(cmap)\r
193     end\r
194 \r
195     # Up/downcasing something that includes this module returns its\r
196     # Up/downcased to_s form\r
197     #\r
198     def downcase\r
199       self.irc_downcase\r
200     end\r
201 \r
202     # We allow up/downcasing with a different casemap\r
203     #\r
204     def irc_upcase(cmap=casemap)\r
205       self.to_s.irc_upcase(cmap)\r
206     end\r
207 \r
208     # Up/downcasing something that includes this module returns its\r
209     # Up/downcased to_s form\r
210     #\r
211     def upcase\r
212       self.irc_upcase\r
213     end\r
214 \r
215   end\r
216 \r
217 end\r
218 \r
219 \r
220 # We start by extending the String class\r
221 # with some IRC-specific methods\r
222 #\r
223 class String\r
224 \r
225   # This method returns the Irc::Casemap whose name is the receiver\r
226   #\r
227   def to_irc_casemap\r
228     Irc::Casemap.get(self) rescue raise TypeError, "Unkown Irc::Casemap #{self.inspect}"\r
229   end\r
230 \r
231   # This method returns a string which is the downcased version of the\r
232   # receiver, according to the given _casemap_\r
233   #\r
234   #\r
235   def irc_downcase(casemap='rfc1459')\r
236     cmap = casemap.to_irc_casemap\r
237     self.tr(cmap.upper, cmap.lower)\r
238   end\r
239 \r
240   # This is the same as the above, except that the string is altered in place\r
241   #\r
242   # See also the discussion about irc_downcase\r
243   #\r
244   def irc_downcase!(casemap='rfc1459')\r
245     cmap = casemap.to_irc_casemap\r
246     self.tr!(cmap.upper, cmap.lower)\r
247   end\r
248 \r
249   # Upcasing functions are provided too\r
250   #\r
251   # See also the discussion about irc_downcase\r
252   #\r
253   def irc_upcase(casemap='rfc1459')\r
254     cmap = casemap.to_irc_casemap\r
255     self.tr(cmap.lower, cmap.upper)\r
256   end\r
257 \r
258   # In-place upcasing\r
259   #\r
260   # See also the discussion about irc_downcase\r
261   #\r
262   def irc_upcase!(casemap='rfc1459')\r
263     cmap = casemap.to_irc_casemap\r
264     self.tr!(cmap.lower, cmap.upper)\r
265   end\r
266 \r
267   # This method checks if the receiver contains IRC glob characters\r
268   #\r
269   # IRC has a very primitive concept of globs: a <tt>*</tt> stands for "any\r
270   # number of arbitrary characters", a <tt>?</tt> stands for "one and exactly\r
271   # one arbitrary character". These characters can be escaped by prefixing them\r
272   # with a slash (<tt>\\</tt>).\r
273   #\r
274   # A known limitation of this glob syntax is that there is no way to escape\r
275   # the escape character itself, so it's not possible to build a glob pattern\r
276   # where the escape character precedes a glob.\r
277   #\r
278   def has_irc_glob?\r
279     self =~ /^[*?]|[^\\][*?]/\r
280   end\r
281 \r
282   # This method is used to convert the receiver into a Regular Expression\r
283   # that matches according to the IRC glob syntax\r
284   #\r
285   def to_irc_regexp\r
286     regmask = Regexp.escape(self)\r
287     regmask.gsub!(/(\\\\)?\\[*?]/) { |m|\r
288       case m\r
289       when /\\(\\[*?])/\r
290         $1\r
291       when /\\\*/\r
292         '.*'\r
293       when /\\\?/\r
294         '.'\r
295       else\r
296         raise "Unexpected match #{m} when converting #{self}"\r
297       end\r
298     }\r
299     Regexp.new(regmask)\r
300   end\r
301 \r
302 end\r
303 \r
304 \r
305 # ArrayOf is a subclass of Array whose elements are supposed to be all\r
306 # of the same class. This is not intended to be used directly, but rather\r
307 # to be subclassed as needed (see for example Irc::UserList and Irc::NetmaskList)\r
308 #\r
309 # Presently, only very few selected methods from Array are overloaded to check\r
310 # if the new elements are the correct class. An orthodox? method is provided\r
311 # to check the entire ArrayOf against the appropriate class.\r
312 #\r
313 class ArrayOf < Array\r
314 \r
315   attr_reader :element_class\r
316 \r
317   # Create a new ArrayOf whose elements are supposed to be all of type _kl_,\r
318   # optionally filling it with the elements from the Array argument.\r
319   #\r
320   def initialize(kl, ar=[])\r
321     raise TypeError, "#{kl.inspect} must be a class name" unless kl.kind_of?(Class)\r
322     super()\r
323     @element_class = kl\r
324     case ar\r
325     when Array\r
326       insert(0, *ar)\r
327     else\r
328       raise TypeError, "#{self.class} can only be initialized from an Array"\r
329     end\r
330   end\r
331 \r
332   def inspect\r
333     "#<#{self.class}[#{@element_class}]:#{'0x%x' % self.object_id}: #{super}>"\r
334   end\r
335 \r
336   # Private method to check the validity of the elements passed to it\r
337   # and optionally raise an error\r
338   #\r
339   # TODO should it accept nils as valid?\r
340   #\r
341   def internal_will_accept?(raising, *els)\r
342     els.each { |el|\r
343       unless el.kind_of?(@element_class)\r
344         raise TypeError, "#{el.inspect} is not of class #{@element_class}" if raising\r
345         return false\r
346       end\r
347     }\r
348     return true\r
349   end\r
350   private :internal_will_accept?\r
351 \r
352   # This method checks if the passed arguments are acceptable for our ArrayOf\r
353   #\r
354   def will_accept?(*els)\r
355     internal_will_accept?(false, *els)\r
356   end\r
357 \r
358   # This method checks that all elements are of the appropriate class\r
359   #\r
360   def valid?\r
361     will_accept?(*self)\r
362   end\r
363 \r
364   # This method is similar to the above, except that it raises an exception\r
365   # if the receiver is not valid\r
366   #\r
367   def validate\r
368     raise TypeError unless valid?\r
369   end\r
370 \r
371   # Overloaded from Array#<<, checks for appropriate class of argument\r
372   #\r
373   def <<(el)\r
374     super(el) if internal_will_accept?(true, el)\r
375   end\r
376 \r
377   # Overloaded from Array#&, checks for appropriate class of argument elements\r
378   #\r
379   def &(ar)\r
380     r = super(ar)\r
381     ArrayOf.new(@element_class, r) if internal_will_accept?(true, *r)\r
382   end\r
383 \r
384   # Overloaded from Array#+, checks for appropriate class of argument elements\r
385   #\r
386   def +(ar)\r
387     ArrayOf.new(@element_class, super(ar)) if internal_will_accept?(true, *ar)\r
388   end\r
389 \r
390   # Overloaded from Array#-, so that an ArrayOf is returned. There is no need\r
391   # to check the validity of the elements in the argument\r
392   #\r
393   def -(ar)\r
394     ArrayOf.new(@element_class, super(ar)) # if internal_will_accept?(true, *ar)\r
395   end\r
396 \r
397   # Overloaded from Array#|, checks for appropriate class of argument elements\r
398   #\r
399   def |(ar)\r
400     ArrayOf.new(@element_class, super(ar)) if internal_will_accept?(true, *ar)\r
401   end\r
402 \r
403   # Overloaded from Array#concat, checks for appropriate class of argument\r
404   # elements\r
405   #\r
406   def concat(ar)\r
407     super(ar) if internal_will_accept?(true, *ar)\r
408   end\r
409 \r
410   # Overloaded from Array#insert, checks for appropriate class of argument\r
411   # elements\r
412   #\r
413   def insert(idx, *ar)\r
414     super(idx, *ar) if internal_will_accept?(true, *ar)\r
415   end\r
416 \r
417   # Overloaded from Array#replace, checks for appropriate class of argument\r
418   # elements\r
419   #\r
420   def replace(ar)\r
421     super(ar) if (ar.kind_of?(ArrayOf) && ar.element_class <= @element_class) or internal_will_accept?(true, *ar)\r
422   end\r
423 \r
424   # Overloaded from Array#push, checks for appropriate class of argument\r
425   # elements\r
426   #\r
427   def push(*ar)\r
428     super(*ar) if internal_will_accept?(true, *ar)\r
429   end\r
430 \r
431   # Overloaded from Array#unshift, checks for appropriate class of argument(s)\r
432   #\r
433   def unshift(*els)\r
434     els.each { |el|\r
435       super(el) if internal_will_accept?(true, *els)\r
436     }\r
437   end\r
438 \r
439   # Modifying methods which we don't handle yet are made private\r
440   #\r
441   private :[]=, :collect!, :map!, :fill, :flatten!\r
442 \r
443 end\r
444 \r
445 \r
446 module Irc\r
447 \r
448 \r
449   # A Netmask identifies each user by collecting its nick, username and\r
450   # hostname in the form <tt>nick!user@host</tt>\r
451   #\r
452   # Netmasks can also contain glob patterns in any of their components; in\r
453   # this form they are used to refer to more than a user or to a user\r
454   # appearing under different forms.\r
455   #\r
456   # Example:\r
457   # * <tt>*!*@*</tt> refers to everybody\r
458   # * <tt>*!someuser@somehost</tt> refers to user +someuser+ on host +somehost+\r
459   #   regardless of the nick used.\r
460   #\r
461   class Netmask\r
462 \r
463     # Netmasks have an associated casemap unless they are bound to a server\r
464     #\r
465     include ServerOrCasemap\r
466 \r
467     attr_reader :nick, :user, :host\r
468 \r
469     # Create a new Netmask from string _str_, which must be in the form\r
470     # _nick_!_user_@_host_\r
471     #\r
472     # It is possible to specify a server or a casemap in the optional Hash:\r
473     # these are used to associate the Netmask with the given server and to set\r
474     # its casemap: if a server is specified and a casemap is not, the server's\r
475     # casemap is used. If both a server and a casemap are specified, the\r
476     # casemap must match the server's casemap or an exception will be raised.\r
477     #\r
478     # Empty +nick+, +user+ or +host+ are converted to the generic glob pattern\r
479     #\r
480     def initialize(str="", opts={})\r
481       # First of all, check for server/casemap option\r
482       #\r
483       init_server_or_casemap(opts)\r
484 \r
485       # Now we can see if the given string _str_ is an actual Netmask\r
486       if str.respond_to?(:to_str)\r
487         case str.to_str\r
488         when /^(?:(\S+?)(?:!(\S+)@(?:(\S+))?)?)?$/\r
489           # We do assignment using our internal methods\r
490           self.nick = $1\r
491           self.user = $2\r
492           self.host = $3\r
493         else\r
494           raise ArgumentError, "#{str.to_str.inspect} does not represent a valid #{self.class}"\r
495         end\r
496       else\r
497         raise TypeError, "#{str} cannot be converted to a #{self.class}"\r
498       end\r
499     end\r
500 \r
501     # A Netmask is easily converted to a String for the usual representation\r
502     #\r
503     def fullform\r
504       "#{nick}!#{user}@#{host}"\r
505     end\r
506     alias :to_s :fullform\r
507 \r
508     # Converts the receiver into a Netmask with the given (optional)\r
509     # server/casemap association. We return self unless a conversion\r
510     # is needed (different casemap/server)\r
511     #\r
512     # Subclasses of Netmask will return a new Netmask\r
513     #\r
514     def to_irc_netmask(opts={})\r
515       if self.class == Netmask\r
516         return self if fits_with_server_and_casemap?(opts)\r
517       end\r
518       return self.fullform.to_irc_netmask(server_and_casemap.merge(opts))\r
519     end\r
520 \r
521     # Converts the receiver into a User with the given (optional)\r
522     # server/casemap association. We return self unless a conversion\r
523     # is needed (different casemap/server)\r
524     #\r
525     def to_irc_user(opts={})\r
526       self.fullform.to_irc_user(server_and_casemap.merge(opts))\r
527     end\r
528 \r
529     # Inspection of a Netmask reveals the server it's bound to (if there is\r
530     # one), its casemap and the nick, user and host part\r
531     #\r
532     def inspect\r
533       str = "<#{self.class}:#{'0x%x' % self.object_id}:"\r
534       str << " @server=#{@server}" if @server\r
535       str << " @nick=#{@nick.inspect} @user=#{@user.inspect}"\r
536       str << " @host=#{@host.inspect} casemap=#{casemap.inspect}>"\r
537       str\r
538     end\r
539 \r
540     # Equality: two Netmasks are equal if they downcase to the same thing\r
541     #\r
542     # TODO we may want it to try other.to_irc_netmask\r
543     #\r
544     def ==(other)\r
545       return false unless other.kind_of?(self.class)\r
546       self.downcase == other.downcase\r
547     end\r
548 \r
549     # This method changes the nick of the Netmask, defaulting to the generic\r
550     # glob pattern if the result is the null string.\r
551     #\r
552     def nick=(newnick)\r
553       @nick = newnick.to_s\r
554       @nick = "*" if @nick.empty?\r
555     end\r
556 \r
557     # This method changes the user of the Netmask, defaulting to the generic\r
558     # glob pattern if the result is the null string.\r
559     #\r
560     def user=(newuser)\r
561       @user = newuser.to_s\r
562       @user = "*" if @user.empty?\r
563     end\r
564 \r
565     # This method changes the hostname of the Netmask, defaulting to the generic\r
566     # glob pattern if the result is the null string.\r
567     #\r
568     def host=(newhost)\r
569       @host = newhost.to_s\r
570       @host = "*" if @host.empty?\r
571     end\r
572 \r
573     # We can replace everything at once with data from another Netmask\r
574     #\r
575     def replace(other)\r
576       case other\r
577       when Netmask\r
578         nick = other.nick\r
579         user = other.user\r
580         host = other.host\r
581         @server = other.server\r
582         @casemap = other.casemap unless @server\r
583       else\r
584         replace(other.to_irc_netmask(server_and_casemap))\r
585       end\r
586     end\r
587 \r
588     # This method checks if a Netmask is definite or not, by seeing if\r
589     # any of its components are defined by globs\r
590     #\r
591     def has_irc_glob?\r
592       return @nick.has_irc_glob? || @user.has_irc_glob? || @host.has_irc_glob?\r
593     end\r
594 \r
595     # This method is used to match the current Netmask against another one\r
596     #\r
597     # The method returns true if each component of the receiver matches the\r
598     # corresponding component of the argument. By _matching_ here we mean\r
599     # that any netmask described by the receiver is also described by the\r
600     # argument.\r
601     #\r
602     # In this sense, matching is rather simple to define in the case when the\r
603     # receiver has no globs: it is just necessary to check if the argument\r
604     # describes the receiver, which can be done by matching it against the\r
605     # argument converted into an IRC Regexp (see String#to_irc_regexp).\r
606     #\r
607     # The situation is also easy when the receiver has globs and the argument\r
608     # doesn't, since in this case the result is false.\r
609     #\r
610     # The more complex case in which both the receiver and the argument have\r
611     # globs is not handled yet.\r
612     #\r
613     def matches?(arg)\r
614       cmp = arg.to_irc_netmask(:casemap => casemap)\r
615       [:nick, :user, :host].each { |component|\r
616         us = self.send(component).irc_downcase(casemap)\r
617         them = cmp.send(component).irc_downcase(casemap)\r
618         raise NotImplementedError if us.has_irc_glob? && them.has_irc_glob?\r
619         return false if us.has_irc_glob? && !them.has_irc_glob?\r
620         return false unless us =~ them.to_irc_regexp\r
621       }\r
622       return true\r
623     end\r
624 \r
625     # Case equality. Checks if arg matches self\r
626     #\r
627     def ===(arg)\r
628       arg.to_irc_netmask(:casemap => casemap).matches?(self)\r
629     end\r
630 \r
631     # Sorting is done via the fullform\r
632     #\r
633     def <=>(arg)\r
634       case arg\r
635       when Netmask\r
636         self.fullform.irc_downcase(casemap) <=> arg.fullform.irc_downcase(casemap)\r
637       else\r
638         self.downcase <=> arg.downcase\r
639       end\r
640     end\r
641 \r
642   end\r
643 \r
644 \r
645   # A NetmaskList is an ArrayOf <code>Netmask</code>s\r
646   #\r
647   class NetmaskList < ArrayOf\r
648 \r
649     # Create a new NetmaskList, optionally filling it with the elements from\r
650     # the Array argument fed to it.\r
651     #\r
652     def initialize(ar=[])\r
653       super(Netmask, ar)\r
654     end\r
655 \r
656   end\r
657 \r
658 end\r
659 \r
660 \r
661 class String\r
662 \r
663   # We keep extending String, this time adding a method that converts a\r
664   # String into an Irc::Netmask object\r
665   #\r
666   def to_irc_netmask(opts={})\r
667     Irc::Netmask.new(self, opts)\r
668   end\r
669 \r
670 end\r
671 \r
672 \r
673 module Irc\r
674 \r
675 \r
676   # An IRC User is identified by his/her Netmask (which must not have globs).\r
677   # In fact, User is just a subclass of Netmask.\r
678   #\r
679   # Ideally, the user and host information of an IRC User should never\r
680   # change, and it shouldn't contain glob patterns. However, IRC is somewhat\r
681   # idiosincratic and it may be possible to know the nick of a User much before\r
682   # its user and host are known. Moreover, some networks (namely Freenode) may\r
683   # change the hostname of a User when (s)he identifies with Nickserv.\r
684   #\r
685   # As a consequence, we must allow changes to a User host and user attributes.\r
686   # We impose a restriction, though: they may not contain glob patterns, except\r
687   # for the special case of an unknown user/host which is represented by a *.\r
688   #\r
689   # It is possible to create a totally unknown User (e.g. for initializations)\r
690   # by setting the nick to * too.\r
691   #\r
692   # TODO list:\r
693   # * see if it's worth to add the other USER data\r
694   # * see if it's worth to add NICKSERV status\r
695   #\r
696   class User < Netmask\r
697     alias :to_s :nick\r
698 \r
699     # Create a new IRC User from a given Netmask (or anything that can be converted\r
700     # into a Netmask) provided that the given Netmask does not have globs.\r
701     #\r
702     def initialize(str="", opts={})\r
703       super\r
704       raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if nick.has_irc_glob? && nick != "*"\r
705       raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if user.has_irc_glob? && user != "*"\r
706       raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if host.has_irc_glob? && host != "*"\r
707       @away = false\r
708     end\r
709 \r
710     # The nick of a User may be changed freely, but it must not contain glob patterns.\r
711     #\r
712     def nick=(newnick)\r
713       raise "Can't change the nick to #{newnick}" if defined?(@nick) and newnick.has_irc_glob?\r
714       super\r
715     end\r
716 \r
717     # We have to allow changing the user of an Irc User due to some networks\r
718     # (e.g. Freenode) changing hostmasks on the fly. We still check if the new\r
719     # user data has glob patterns though.\r
720     #\r
721     def user=(newuser)\r
722       raise "Can't change the username to #{newuser}" if defined?(@user) and newuser.has_irc_glob?\r
723       super\r
724     end\r
725 \r
726     # We have to allow changing the host of an Irc User due to some networks\r
727     # (e.g. Freenode) changing hostmasks on the fly. We still check if the new\r
728     # host data has glob patterns though.\r
729     #\r
730     def host=(newhost)\r
731       raise "Can't change the hostname to #{newhost}" if defined?(@host) and newhost.has_irc_glob?\r
732       super\r
733     end\r
734 \r
735     # Checks if a User is well-known or not by looking at the hostname and user\r
736     #\r
737     def known?\r
738       return nick!= "*" && user!="*" && host!="*"\r
739     end\r
740 \r
741     # Is the user away?\r
742     #\r
743     def away?\r
744       return @away\r
745     end\r
746 \r
747     # Set the away status of the user. Use away=(nil) or away=(false)\r
748     # to unset away\r
749     #\r
750     def away=(msg="")\r
751       if msg\r
752         @away = msg\r
753       else\r
754         @away = false\r
755       end\r
756     end\r
757 \r
758     # Since to_irc_user runs the same checks on server and channel as\r
759     # to_irc_netmask, we just try that and return self if it works.\r
760     #\r
761     # Subclasses of User will return self if possible.\r
762     #\r
763     def to_irc_user(opts={})\r
764       return self if fits_with_server_and_casemap?(opts)\r
765       return self.fullform.to_irc_user(server_and_casemap(opts))\r
766     end\r
767 \r
768     # We can replace everything at once with data from another User\r
769     #\r
770     def replace(other)\r
771       case other\r
772       when User\r
773         nick = other.nick\r
774         user = other.user\r
775         host = other.host\r
776         @server = other.server\r
777         @casemap = other.casemap unless @server\r
778         @away = other.away\r
779       else\r
780         replace(other.to_irc_user(server_and_casemap))\r
781       end\r
782     end\r
783 \r
784   end\r
785 \r
786 \r
787   # A UserList is an ArrayOf <code>User</code>s\r
788   #\r
789   class UserList < ArrayOf\r
790 \r
791     # Create a new UserList, optionally filling it with the elements from\r
792     # the Array argument fed to it.\r
793     #\r
794     def initialize(ar=[])\r
795       super(User, ar)\r
796     end\r
797 \r
798   end\r
799 \r
800 end\r
801 \r
802 class String\r
803 \r
804   # We keep extending String, this time adding a method that converts a\r
805   # String into an Irc::User object\r
806   #\r
807   def to_irc_user(opts={})\r
808     Irc::User.new(self, opts)\r
809   end\r
810 \r
811 end\r
812 \r
813 module Irc\r
814 \r
815   # An IRC Channel is identified by its name, and it has a set of properties:\r
816   # * a Channel::Topic\r
817   # * a UserList\r
818   # * a set of Channel::Modes\r
819   #\r
820   # The Channel::Topic and Channel::Mode classes are defined within the\r
821   # Channel namespace because they only make sense there\r
822   #\r
823   class Channel\r
824 \r
825 \r
826     # Mode on a Channel\r
827     #\r
828     class Mode\r
829       def initialize(ch)\r
830         @channel = ch\r
831       end\r
832 \r
833     end\r
834 \r
835 \r
836     # Channel modes of type A manipulate lists\r
837     #\r
838     class ModeTypeA < Mode\r
839       def initialize(ch)\r
840         super\r
841         @list = NetmaskList.new\r
842       end\r
843 \r
844       def set(val)\r
845         nm = @channel.server.new_netmask(val)\r
846         @list << nm unless @list.include?(nm)\r
847       end\r
848 \r
849       def reset(val)\r
850         nm = @channel.server.new_netmask(val)\r
851         @list.delete(nm)\r
852       end\r
853 \r
854     end\r
855 \r
856 \r
857     # Channel modes of type B need an argument\r
858     #\r
859     class ModeTypeB < Mode\r
860       def initialize(ch)\r
861         super\r
862         @arg = nil\r
863       end\r
864 \r
865       def set(val)\r
866         @arg = val\r
867       end\r
868 \r
869       def reset(val)\r
870         @arg = nil if @arg == val\r
871       end\r
872 \r
873     end\r
874 \r
875 \r
876     # Channel modes that change the User prefixes are like\r
877     # Channel modes of type B, except that they manipulate\r
878     # lists of Users, so they are somewhat similar to channel\r
879     # modes of type A\r
880     #\r
881     class UserMode < ModeTypeB\r
882       def initialize(ch)\r
883         super\r
884         @list = UserList.new\r
885       end\r
886 \r
887       def set(val)\r
888         u = @channel.server.user(val)\r
889         @list << u unless @list.include?(u)\r
890       end\r
891 \r
892       def reset(val)\r
893         u = @channel.server.user(val)\r
894         @list.delete(u)\r
895       end\r
896 \r
897     end\r
898 \r
899 \r
900     # Channel modes of type C need an argument when set,\r
901     # but not when they get reset\r
902     #\r
903     class ModeTypeC < Mode\r
904       def initialize(ch)\r
905         super\r
906         @arg = false\r
907       end\r
908 \r
909       def status\r
910         @arg\r
911       end\r
912 \r
913       def set(val)\r
914         @arg = val\r
915       end\r
916 \r
917       def reset\r
918         @arg = false\r
919       end\r
920 \r
921     end\r
922 \r
923 \r
924     # Channel modes of type D are basically booleans\r
925     #\r
926     class ModeTypeD < Mode\r
927       def initialize(ch)\r
928         super\r
929         @set = false\r
930       end\r
931 \r
932       def set?\r
933         return @set\r
934       end\r
935 \r
936       def set\r
937         @set = true\r
938       end\r
939 \r
940       def reset\r
941         @set = false\r
942       end\r
943 \r
944     end\r
945 \r
946 \r
947     # A Topic represents the topic of a channel. It consists of\r
948     # the topic itself, who set it and when\r
949     #\r
950     class Topic\r
951       attr_accessor :text, :set_by, :set_on\r
952       alias :to_s :text\r
953 \r
954       # Create a new Topic setting the text, the creator and\r
955       # the creation time\r
956       #\r
957       def initialize(text="", set_by="", set_on=Time.new)\r
958         @text = text\r
959         @set_by = set_by.to_irc_user\r
960         @set_on = set_on\r
961       end\r
962 \r
963       # Replace a Topic with another one\r
964       #\r
965       def replace(topic)\r
966         raise TypeError, "#{topic.inspect} is not of class #{self.class}" unless topic.kind_of?(self.class)\r
967         @text = topic.text.dup\r
968         @set_by = topic.set_by.dup\r
969         @set_on = topic.set_on.dup\r
970       end\r
971 \r
972       # Returns self\r
973       #\r
974       def to_irc_channel_topic\r
975         self\r
976       end\r
977 \r
978     end\r
979 \r
980   end\r
981 \r
982 end\r
983 \r
984 \r
985 class String\r
986 \r
987   # Returns an Irc::Channel::Topic with self as text\r
988   #\r
989   def to_irc_channel_topic\r
990     Irc::Channel::Topic.new(self)\r
991   end\r
992 \r
993 end\r
994 \r
995 \r
996 module Irc\r
997 \r
998 \r
999   # Here we start with the actual Channel class\r
1000   #\r
1001   class Channel\r
1002 \r
1003     include ServerOrCasemap\r
1004     attr_reader :name, :topic, :mode, :users\r
1005     alias :to_s :name\r
1006 \r
1007     def inspect\r
1008       str = "<#{self.class}:#{'0x%x' % self.object_id}:"\r
1009       str << " on server #{server}" if server\r
1010       str << " @name=#{@name.inspect} @topic=#{@topic.text.inspect}"\r
1011       str << " @users=<#{@users.sort.join(', ')}>"\r
1012       str\r
1013     end\r
1014 \r
1015     # Returns self\r
1016     #\r
1017     def to_irc_channel\r
1018       self\r
1019     end\r
1020 \r
1021     # Creates a new channel with the given name, optionally setting the topic\r
1022     # and an initial users list.\r
1023     #\r
1024     # No additional info is created here, because the channel flags and userlists\r
1025     # allowed depend on the server.\r
1026     #\r
1027     def initialize(name, topic=nil, users=[], opts={})\r
1028       raise ArgumentError, "Channel name cannot be empty" if name.to_s.empty?\r
1029       warn "Unknown channel prefix #{name[0].chr}" if name !~ /^[&#+!]/\r
1030       raise ArgumentError, "Invalid character in #{name.inspect}" if name =~ /[ \x07,]/\r
1031 \r
1032       init_server_or_casemap(opts)\r
1033 \r
1034       @name = name\r
1035 \r
1036       @topic = (topic.to_irc_channel_topic rescue Channel::Topic.new)\r
1037 \r
1038       @users = UserList.new\r
1039 \r
1040       users.each { |u|\r
1041         @users << u.to_irc_user(server_and_casemap)\r
1042       }\r
1043 \r
1044       # Flags\r
1045       @mode = {}\r
1046     end\r
1047 \r
1048     # Removes a user from the channel\r
1049     #\r
1050     def delete_user(user)\r
1051       @mode.each { |sym, mode|\r
1052         mode.reset(user) if mode.kind_of?(UserMode)\r
1053       }\r
1054       @users.delete(user)\r
1055     end\r
1056 \r
1057     # The channel prefix\r
1058     #\r
1059     def prefix\r
1060       name[0].chr\r
1061     end\r
1062 \r
1063     # A channel is local to a server if it has the '&' prefix\r
1064     #\r
1065     def local?\r
1066       name[0] = 0x26\r
1067     end\r
1068 \r
1069     # A channel is modeless if it has the '+' prefix\r
1070     #\r
1071     def modeless?\r
1072       name[0] = 0x2b\r
1073     end\r
1074 \r
1075     # A channel is safe if it has the '!' prefix\r
1076     #\r
1077     def safe?\r
1078       name[0] = 0x21\r
1079     end\r
1080 \r
1081     # A channel is normal if it has the '#' prefix\r
1082     #\r
1083     def normal?\r
1084       name[0] = 0x23\r
1085     end\r
1086 \r
1087     # Create a new mode\r
1088     #\r
1089     def create_mode(sym, kl)\r
1090       @mode[sym.to_sym] = kl.new(self)\r
1091     end\r
1092 \r
1093   end\r
1094 \r
1095 \r
1096   # A ChannelList is an ArrayOf <code>Channel</code>s\r
1097   #\r
1098   class ChannelList < ArrayOf\r
1099 \r
1100     # Create a new ChannelList, optionally filling it with the elements from\r
1101     # the Array argument fed to it.\r
1102     #\r
1103     def initialize(ar=[])\r
1104       super(Channel, ar)\r
1105     end\r
1106 \r
1107   end\r
1108 \r
1109 end\r
1110 \r
1111 \r
1112 class String\r
1113 \r
1114   # We keep extending String, this time adding a method that converts a\r
1115   # String into an Irc::Channel object\r
1116   #\r
1117   def to_irc_channel(opts={})\r
1118     Irc::Channel.new(self, opts)\r
1119   end\r
1120 \r
1121 end\r
1122 \r
1123 \r
1124 module Irc\r
1125 \r
1126 \r
1127   # An IRC Server represents the Server the client is connected to.\r
1128   #\r
1129   class Server\r
1130 \r
1131     attr_reader :hostname, :version, :usermodes, :chanmodes\r
1132     alias :to_s :hostname\r
1133     attr_reader :supports, :capabilities\r
1134 \r
1135     attr_reader :channels, :users\r
1136 \r
1137     def channel_names\r
1138       @channels.map { |ch| ch.downcase }\r
1139     end\r
1140 \r
1141     def user_nicks\r
1142       @users.map { |u| u.downcase }\r
1143     end\r
1144 \r
1145     def inspect\r
1146       chans, users = [@channels, @users].map {|d|\r
1147         d.sort { |a, b|\r
1148           a.downcase <=> b.downcase\r
1149         }.map { |x|\r
1150           x.inspect\r
1151         }\r
1152       }\r
1153 \r
1154       str = "<#{self.class}:#{'0x%x' % self.object_id}:"\r
1155       str << " @hostname=#{hostname}"\r
1156       str << " @channels=#{chans}"\r
1157       str << " @users=#{users}>"\r
1158       str\r
1159     end\r
1160 \r
1161     # Create a new Server, with all instance variables reset to nil (for\r
1162     # scalar variables), empty channel and user lists and @supports\r
1163     # initialized to the default values for all known supported features.\r
1164     #\r
1165     def initialize\r
1166       @hostname = @version = @usermodes = @chanmodes = nil\r
1167 \r
1168       @channels = ChannelList.new\r
1169 \r
1170       @users = UserList.new\r
1171 \r
1172       reset_capabilities\r
1173     end\r
1174 \r
1175     # Resets the server capabilities\r
1176     #\r
1177     def reset_capabilities\r
1178       @supports = {\r
1179         :casemapping => 'rfc1459',\r
1180         :chanlimit => {},\r
1181         :chanmodes => {\r
1182           :typea => nil, # Type A: address lists\r
1183           :typeb => nil, # Type B: needs a parameter\r
1184           :typec => nil, # Type C: needs a parameter when set\r
1185           :typed => nil  # Type D: must not have a parameter\r
1186         },\r
1187         :channellen => 200,\r
1188         :chantypes => "#&",\r
1189         :excepts => nil,\r
1190         :idchan => {},\r
1191         :invex => nil,\r
1192         :kicklen => nil,\r
1193         :maxlist => {},\r
1194         :modes => 3,\r
1195         :network => nil,\r
1196         :nicklen => 9,\r
1197         :prefix => {\r
1198           :modes => 'ov'.scan(/./),\r
1199           :prefixes => '@+'.scan(/./)\r
1200         },\r
1201         :safelist => nil,\r
1202         :statusmsg => nil,\r
1203         :std => nil,\r
1204         :targmax => {},\r
1205         :topiclen => nil\r
1206       }\r
1207       @capabilities = {}\r
1208     end\r
1209 \r
1210     # Resets the Channel and User list\r
1211     #\r
1212     def reset_lists\r
1213       @users.each { |u|\r
1214         delete_user(u)\r
1215       }\r
1216       @channels.each { |u|\r
1217         delete_channel(u)\r
1218       }\r
1219     end\r
1220 \r
1221     # Clears the server\r
1222     #\r
1223     def clear\r
1224       reset_lists\r
1225       reset_capabilities\r
1226     end\r
1227 \r
1228     # This method is used to parse a 004 RPL_MY_INFO line\r
1229     #\r
1230     def parse_my_info(line)\r
1231       ar = line.split(' ')\r
1232       @hostname = ar[0]\r
1233       @version = ar[1]\r
1234       @usermodes = ar[2]\r
1235       @chanmodes = ar[3]\r
1236     end\r
1237 \r
1238     def noval_warn(key, val, &block)\r
1239       if val\r
1240         yield if block_given?\r
1241       else\r
1242         warn "No #{key.to_s.upcase} value"\r
1243       end\r
1244     end\r
1245 \r
1246     def val_warn(key, val, &block)\r
1247       if val == true or val == false or val.nil?\r
1248         yield if block_given?\r
1249       else\r
1250         warn "No #{key.to_s.upcase} value must be specified, got #{val}"\r
1251       end\r
1252     end\r
1253     private :noval_warn, :val_warn\r
1254 \r
1255     # This method is used to parse a 005 RPL_ISUPPORT line\r
1256     #\r
1257     # See the RPL_ISUPPORT draft[http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt]\r
1258     #\r
1259     def parse_isupport(line)\r
1260       debug "Parsing ISUPPORT #{line.inspect}"\r
1261       ar = line.split(' ')\r
1262       reparse = ""\r
1263       ar.each { |en|\r
1264         prekey, val = en.split('=', 2)\r
1265         if prekey =~ /^-(.*)/\r
1266           key = $1.downcase.to_sym\r
1267           val = false\r
1268         else\r
1269           key = prekey.downcase.to_sym\r
1270         end\r
1271         case key\r
1272         when :casemapping, :network\r
1273           noval_warn(key, val) {\r
1274             @supports[key] = val\r
1275           }\r
1276         when :chanlimit, :idchan, :maxlist, :targmax\r
1277           noval_warn(key, val) {\r
1278             groups = val.split(',')\r
1279             groups.each { |g|\r
1280               k, v = g.split(':')\r
1281               @supports[key][k] = v.to_i\r
1282             }\r
1283           }\r
1284         when :maxchannels\r
1285           noval_warn(key, val) {\r
1286             reparse += "CHANLIMIT=(chantypes):#{val} "\r
1287           }\r
1288         when :maxtargets\r
1289           noval_warn(key, val) {\r
1290             @supports[key]['PRIVMSG'] = val.to_i\r
1291             @supports[key]['NOTICE'] = val.to_i\r
1292           }\r
1293         when :chanmodes\r
1294           noval_warn(key, val) {\r
1295             groups = val.split(',')\r
1296             @supports[key][:typea] = groups[0].scan(/./).map { |x| x.to_sym}\r
1297             @supports[key][:typeb] = groups[1].scan(/./).map { |x| x.to_sym}\r
1298             @supports[key][:typec] = groups[2].scan(/./).map { |x| x.to_sym}\r
1299             @supports[key][:typed] = groups[3].scan(/./).map { |x| x.to_sym}\r
1300           }\r
1301         when :channellen, :kicklen, :modes, :topiclen\r
1302           if val\r
1303             @supports[key] = val.to_i\r
1304           else\r
1305             @supports[key] = nil\r
1306           end\r
1307         when :chantypes\r
1308           @supports[key] = val # can also be nil\r
1309         when :excepts\r
1310           val ||= 'e'\r
1311           @supports[key] = val\r
1312         when :invex\r
1313           val ||= 'I'\r
1314           @supports[key] = val\r
1315         when :nicklen\r
1316           noval_warn(key, val) {\r
1317             @supports[key] = val.to_i\r
1318           }\r
1319         when :prefix\r
1320           if val\r
1321             val.scan(/\((.*)\)(.*)/) { |m, p|\r
1322               @supports[key][:modes] = m.scan(/./).map { |x| x.to_sym}\r
1323               @supports[key][:prefixes] = p.scan(/./).map { |x| x.to_sym}\r
1324             }\r
1325           else\r
1326             @supports[key][:modes] = nil\r
1327             @supports[key][:prefixes] = nil\r
1328           end\r
1329         when :safelist\r
1330           val_warn(key, val) {\r
1331             @supports[key] = val.nil? ? true : val\r
1332           }\r
1333         when :statusmsg\r
1334           noval_warn(key, val) {\r
1335             @supports[key] = val.scan(/./)\r
1336           }\r
1337         when :std\r
1338           noval_warn(key, val) {\r
1339             @supports[key] = val.split(',')\r
1340           }\r
1341         else\r
1342           @supports[key] =  val.nil? ? true : val\r
1343         end\r
1344       }\r
1345       reparse.gsub!("(chantypes)",@supports[:chantypes])\r
1346       parse_isupport(reparse) unless reparse.empty?\r
1347     end\r
1348 \r
1349     # Returns the casemap of the server.\r
1350     #\r
1351     def casemap\r
1352       @supports[:casemapping]\r
1353     end\r
1354 \r
1355     # Returns User or Channel depending on what _name_ can be\r
1356     # a name of\r
1357     #\r
1358     def user_or_channel?(name)\r
1359       if supports[:chantypes].include?(name[0])\r
1360         return Channel\r
1361       else\r
1362         return User\r
1363       end\r
1364     end\r
1365 \r
1366     # Returns the actual User or Channel object matching _name_\r
1367     #\r
1368     def user_or_channel(name)\r
1369       if supports[:chantypes].include?(name[0])\r
1370         return channel(name)\r
1371       else\r
1372         return user(name)\r
1373       end\r
1374     end\r
1375 \r
1376     # Checks if the receiver already has a channel with the given _name_\r
1377     #\r
1378     def has_channel?(name)\r
1379       channel_names.index(name.downcase)\r
1380     end\r
1381     alias :has_chan? :has_channel?\r
1382 \r
1383     # Returns the channel with name _name_, if available\r
1384     #\r
1385     def get_channel(name)\r
1386       idx = has_channel?(name)\r
1387       channels[idx] if idx\r
1388     end\r
1389     alias :get_chan :get_channel\r
1390 \r
1391     # Create a new Channel object bound to the receiver and add it to the\r
1392     # list of <code>Channel</code>s on the receiver, unless the channel was\r
1393     # present already. In this case, the default action is to raise an\r
1394     # exception, unless _fails_ is set to false\r
1395     #\r
1396     def new_channel(name, topic=nil, users=[], fails=true)\r
1397       ex = get_chan(name)\r
1398       if ex\r
1399         raise "Channel #{name} already exists on server #{self}" if fails\r
1400         return ex\r
1401       else\r
1402 \r
1403         prefix = name[0].chr\r
1404 \r
1405         # Give a warning if the new Channel goes over some server limits.\r
1406         #\r
1407         # FIXME might need to raise an exception\r
1408         #\r
1409         warn "#{self} doesn't support channel prefix #{prefix}" unless @supports[:chantypes].include?(prefix)\r
1410         warn "#{self} doesn't support channel names this long (#{name.length} > #{@supports[:channellen]})" unless name.length <= @supports[:channellen]\r
1411 \r
1412         # Next, we check if we hit the limit for channels of type +prefix+\r
1413         # if the server supports +chanlimit+\r
1414         #\r
1415         @supports[:chanlimit].keys.each { |k|\r
1416           next unless k.include?(prefix)\r
1417           count = 0\r
1418           channel_names.each { |n|\r
1419             count += 1 if k.include?(n[0])\r
1420           }\r
1421           raise IndexError, "Already joined #{count} channels with prefix #{k}" if count == @supports[:chanlimit][k]\r
1422         }\r
1423 \r
1424         # So far, everything is fine. Now create the actual Channel\r
1425         #\r
1426         chan = Channel.new(name, topic, users, :server => self)\r
1427 \r
1428         # We wade through +prefix+ and +chanmodes+ to create appropriate\r
1429         # lists and flags for this channel\r
1430 \r
1431         @supports[:prefix][:modes].each { |mode|\r
1432           chan.create_mode(mode, Channel::UserMode)\r
1433         } if @supports[:prefix][:modes]\r
1434 \r
1435         @supports[:chanmodes].each { |k, val|\r
1436           if val\r
1437             case k\r
1438             when :typea\r
1439               val.each { |mode|\r
1440                 chan.create_mode(mode, Channel::ModeTypeA)\r
1441               }\r
1442             when :typeb\r
1443               val.each { |mode|\r
1444                 chan.create_mode(mode, Channel::ModeTypeB)\r
1445               }\r
1446             when :typec\r
1447               val.each { |mode|\r
1448                 chan.create_mode(mode, Channel::ModeTypeC)\r
1449               }\r
1450             when :typed\r
1451               val.each { |mode|\r
1452                 chan.create_mode(mode, Channel::ModeTypeD)\r
1453               }\r
1454             end\r
1455           end\r
1456         }\r
1457 \r
1458         @channels << chan\r
1459         # debug "Created channel #{chan.inspect}"\r
1460         return chan\r
1461       end\r
1462     end\r
1463 \r
1464     # Returns the Channel with the given _name_ on the server,\r
1465     # creating it if necessary. This is a short form for\r
1466     # new_channel(_str_, nil, [], +false+)\r
1467     #\r
1468     def channel(str)\r
1469       new_channel(str,nil,[],false)\r
1470     end\r
1471 \r
1472     # Remove Channel _name_ from the list of <code>Channel</code>s\r
1473     #\r
1474     def delete_channel(name)\r
1475       idx = has_channel?(name)\r
1476       raise "Tried to remove unmanaged channel #{name}" unless idx\r
1477       @channels.delete_at(idx)\r
1478     end\r
1479 \r
1480     # Checks if the receiver already has a user with the given _nick_\r
1481     #\r
1482     def has_user?(nick)\r
1483       user_nicks.index(nick.downcase)\r
1484     end\r
1485 \r
1486     # Returns the user with nick _nick_, if available\r
1487     #\r
1488     def get_user(nick)\r
1489       idx = has_user?(nick)\r
1490       @users[idx] if idx\r
1491     end\r
1492 \r
1493     # Create a new User object bound to the receiver and add it to the list\r
1494     # of <code>User</code>s on the receiver, unless the User was present\r
1495     # already. In this case, the default action is to raise an exception,\r
1496     # unless _fails_ is set to false\r
1497     #\r
1498     def new_user(str, fails=true)\r
1499       tmp = str.to_irc_user(:server => self)\r
1500       old = get_user(tmp.nick)\r
1501       if old\r
1502         # debug "User already existed as #{old.inspect}"\r
1503         if tmp.known?\r
1504           if old.known?\r
1505             # Do not raise an error: things like Freenode change the hostname after identification\r
1506             warning "User #{tmp.nick} has inconsistent Netmasks! #{self} knows #{old.inspect} but access was tried with #{tmp.inspect}" if old != tmp\r
1507             raise "User #{tmp} already exists on server #{self}" if fails\r
1508           end\r
1509           if old != tmp\r
1510             old.replace(tmp)\r
1511             # debug "User improved to #{old.inspect}"\r
1512           end\r
1513         end\r
1514         return old\r
1515       else\r
1516         warn "#{self} doesn't support nicknames this long (#{tmp.nick.length} > #{@supports[:nicklen]})" unless tmp.nick.length <= @supports[:nicklen]\r
1517         @users << tmp\r
1518         return @users.last\r
1519       end\r
1520     end\r
1521 \r
1522     # Returns the User with the given Netmask on the server,\r
1523     # creating it if necessary. This is a short form for\r
1524     # new_user(_str_, +false+)\r
1525     #\r
1526     def user(str)\r
1527       new_user(str, false)\r
1528     end\r
1529 \r
1530     # Deletes User _user_ from Channel _channel_\r
1531     #\r
1532     def delete_user_from_channel(user, channel)\r
1533       channel.delete_user(user)\r
1534     end\r
1535 \r
1536     # Remove User _someuser_ from the list of <code>User</code>s.\r
1537     # _someuser_ must be specified with the full Netmask.\r
1538     #\r
1539     def delete_user(someuser)\r
1540       idx = has_user?(someuser)\r
1541       raise "Tried to remove unmanaged user #{user}" unless idx\r
1542       have = self.user(someuser)\r
1543       @channels.each { |ch|\r
1544         delete_user_from_channel(have, ch)\r
1545       }\r
1546       @users.delete_at(idx)\r
1547     end\r
1548 \r
1549     # Create a new Netmask object with the appropriate casemap\r
1550     #\r
1551     def new_netmask(str)\r
1552       str.to_irc_netmask(:server => self)\r
1553     end\r
1554 \r
1555     # Finds all <code>User</code>s on server whose Netmask matches _mask_\r
1556     #\r
1557     def find_users(mask)\r
1558       nm = new_netmask(mask)\r
1559       @users.inject(UserList.new) {\r
1560         |list, user|\r
1561         if user.user == "*" or user.host == "*"\r
1562           list << user if user.nick.downcase =~ nm.nick.downcase.to_irc_regexp\r
1563         else\r
1564           list << user if user.matches?(nm)\r
1565         end\r
1566         list\r
1567       }\r
1568     end\r
1569 \r
1570   end\r
1571 \r
1572 end\r
1573 \r