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