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