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