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