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