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