Improve robustness of ArrayOf; fix some inspect methods
[rbot] / lib / rbot / irc.rb
1 #-- vim:sw=2:et\r
2 # General TODO list\r
3 # * do we want to handle a Channel list for each User telling which\r
4 #   Channels is the User on (of those the client is on too)?\r
5 #   We may want this so that when a User leaves all Channels and he hasn't\r
6 #   sent us privmsgs, we know remove him from the Server @users list\r
7 #++\r
8 # :title: IRC module\r
9 #\r
10 # Basic IRC stuff\r
11 #\r
12 # This module defines the fundamental building blocks for IRC\r
13 #\r
14 # Author:: Giuseppe Bilotta (giuseppe.bilotta@gmail.com)\r
15 # Copyright:: Copyright (c) 2006 Giuseppe Bilotta\r
16 # License:: GPLv2\r
17 #\r
18 # TODO User should have associated Server too\r
19 #\r
20 # TODO rather than the complex init methods, we should provide a single one (having a String parameter)\r
21 # and then provide to_irc_netmask(casemap), to_irc_user(server), to_irc_channel(server) etc\r
22 \r
23 \r
24 # We start by extending the String class\r
25 # with some IRC-specific methods\r
26 #\r
27 class String\r
28 \r
29   # This method returns a string which is the downcased version of the\r
30   # receiver, according to IRC rules: due to the Scandinavian origin of IRC,\r
31   # the characters <tt>{}|^</tt> are considered the uppercase equivalent of\r
32   # <tt>[]\~</tt>.\r
33   #\r
34   # Since IRC is mostly case-insensitive (the Windows way: case is preserved,\r
35   # but it's actually ignored to check equality), this method is rather\r
36   # important when checking if two strings refer to the same entity\r
37   # (User/Channel)\r
38   #\r
39   # Modern server allow different casemaps, too, in which some or all\r
40   # of the extra characters are not converted\r
41   #\r
42   def irc_downcase(casemap='rfc1459')\r
43     case casemap\r
44     when 'rfc1459'\r
45       self.tr("\x41-\x5e", "\x61-\x7e")\r
46     when 'strict-rfc1459'\r
47       self.tr("\x41-\x5d", "\x61-\x7d")\r
48     when 'ascii'\r
49       self.tr("\x41-\x5a", "\x61-\x7a")\r
50     else\r
51       raise TypeError, "Unknown casemap #{casemap}"\r
52     end\r
53   end\r
54 \r
55   # This is the same as the above, except that the string is altered in place\r
56   #\r
57   # See also the discussion about irc_downcase\r
58   #\r
59   def irc_downcase!(casemap='rfc1459')\r
60     case casemap\r
61     when 'rfc1459'\r
62       self.tr!("\x41-\x5e", "\x61-\x7e")\r
63     when 'strict-rfc1459'\r
64       self.tr!("\x41-\x5d", "\x61-\x7d")\r
65     when 'ascii'\r
66       self.tr!("\x41-\x5a", "\x61-\x7a")\r
67     else\r
68       raise TypeError, "Unknown casemap #{casemap}"\r
69     end\r
70   end\r
71 \r
72   # Upcasing functions are provided too\r
73   #\r
74   # See also the discussion about irc_downcase\r
75   #\r
76   def irc_upcase(casemap='rfc1459')\r
77     case casemap\r
78     when 'rfc1459'\r
79       self.tr("\x61-\x7e", "\x41-\x5e")\r
80     when 'strict-rfc1459'\r
81       self.tr("\x61-\x7d", "\x41-\x5d")\r
82     when 'ascii'\r
83       self.tr("\x61-\x7a", "\x41-\x5a")\r
84     else\r
85       raise TypeError, "Unknown casemap #{casemap}"\r
86     end\r
87   end\r
88 \r
89   # In-place upcasing\r
90   #\r
91   # See also the discussion about irc_downcase\r
92   #\r
93   def irc_upcase!(casemap='rfc1459')\r
94     case casemap\r
95     when 'rfc1459'\r
96       self.tr!("\x61-\x7e", "\x41-\x5e")\r
97     when 'strict-rfc1459'\r
98       self.tr!("\x61-\x7d", "\x41-\x5d")\r
99     when 'ascii'\r
100       self.tr!("\x61-\x7a", "\x41-\x5a")\r
101     else\r
102       raise TypeError, "Unknown casemap #{casemap}"\r
103     end\r
104   end\r
105 \r
106   # This method checks if the receiver contains IRC glob characters\r
107   #\r
108   # IRC has a very primitive concept of globs: a <tt>*</tt> stands for "any\r
109   # number of arbitrary characters", a <tt>?</tt> stands for "one and exactly\r
110   # one arbitrary character". These characters can be escaped by prefixing them\r
111   # with a slash (<tt>\\</tt>).\r
112   #\r
113   # A known limitation of this glob syntax is that there is no way to escape\r
114   # the escape character itself, so it's not possible to build a glob pattern\r
115   # where the escape character precedes a glob.\r
116   #\r
117   def has_irc_glob?\r
118     self =~ /^[*?]|[^\\][*?]/\r
119   end\r
120 \r
121   # This method is used to convert the receiver into a Regular Expression\r
122   # that matches according to the IRC glob syntax\r
123   #\r
124   def to_irc_regexp\r
125     regmask = Regexp.escape(self)\r
126     regmask.gsub!(/(\\\\)?\\[*?]/) { |m|\r
127       case m\r
128       when /\\(\\[*?])/\r
129         $1\r
130       when /\\\*/\r
131         '.*'\r
132       when /\\\?/\r
133         '.'\r
134       else\r
135         raise "Unexpected match #{m} when converting #{self}"\r
136       end\r
137     }\r
138     Regexp.new(regmask)\r
139   end\r
140 end\r
141 \r
142 \r
143 # ArrayOf is a subclass of Array whose elements are supposed to be all\r
144 # of the same class. This is not intended to be used directly, but rather\r
145 # to be subclassed as needed (see for example Irc::UserList and Irc::NetmaskList)\r
146 #\r
147 # Presently, only very few selected methods from Array are overloaded to check\r
148 # if the new elements are the correct class. An orthodox? method is provided\r
149 # to check the entire ArrayOf against the appropriate class.\r
150 #\r
151 class ArrayOf < Array\r
152 \r
153   attr_reader :element_class\r
154 \r
155   # Create a new ArrayOf whose elements are supposed to be all of type _kl_,\r
156   # optionally filling it with the elements from the Array argument.\r
157   #\r
158   def initialize(kl, ar=[])\r
159     raise TypeError, "#{kl.inspect} must be a class name" unless kl.kind_of?(Class)\r
160     super()\r
161     @element_class = kl\r
162     case ar\r
163     when Array\r
164       insert(0, *ar)\r
165     else\r
166       raise TypeError, "#{self.class} can only be initialized from an Array"\r
167     end\r
168   end\r
169 \r
170   def inspect\r
171     "#<#{self.class}[#{@element_class}]:#{'0x%x' % self.object_id}: #{super}>"\r
172   end\r
173 \r
174   # Private method to check the validity of the elements passed to it\r
175   # and optionally raise an error\r
176   #\r
177   # TODO should it accept nils as valid?\r
178   #\r
179   def internal_will_accept?(raising, *els)\r
180     els.each { |el|\r
181       unless el.kind_of?(@element_class)\r
182         raise TypeError, "#{el.inspect} is not of class #{@element_class}" if raising\r
183         return false\r
184       end\r
185     }\r
186     return true\r
187   end\r
188   private :internal_will_accept?\r
189 \r
190   # This method checks if the passed arguments are acceptable for our ArrayOf\r
191   #\r
192   def will_accept?(*els)\r
193     internal_will_accept?(false, *els)\r
194   end\r
195 \r
196   # This method checks that all elements are of the appropriate class\r
197   #\r
198   def valid?\r
199     will_accept?(*self)\r
200   end\r
201 \r
202   # This method is similar to the above, except that it raises an exception\r
203   # if the receiver is not valid\r
204   def validate\r
205     raise TypeError unless valid?\r
206   end\r
207 \r
208   # Overloaded from Array#<<, checks for appropriate class of argument\r
209   #\r
210   def <<(el)\r
211     super(el) if internal_will_accept?(true, el)\r
212   end\r
213 \r
214   # Overloaded from Array#&, checks for appropriate class of argument elements\r
215   #\r
216   def &(ar)\r
217     r = super(ar)\r
218     ArrayOf.new(@element_class, r) if internal_will_accept?(true, *r)\r
219   end\r
220 \r
221   # Overloaded from Array#+, checks for appropriate class of argument elements\r
222   #\r
223   def +(ar)\r
224     ArrayOf.new(@element_class, super(ar)) if internal_will_accept?(true, *ar)\r
225   end\r
226 \r
227   # Overloaded from Array#-, so that an ArrayOf is returned. There is no need\r
228   # to check the validity of the elements in the argument\r
229   #\r
230   def -(ar)\r
231     ArrayOf.new(@element_class, super(ar)) # if internal_will_accept?(true, *ar)\r
232   end\r
233 \r
234   # Overloaded from Array#|, checks for appropriate class of argument elements\r
235   #\r
236   def |(ar)\r
237     ArrayOf.new(@element_class, super(ar)) if internal_will_accept?(true, *ar)\r
238   end\r
239 \r
240   # Overloaded from Array#concat, checks for appropriate class of argument\r
241   # elements\r
242   #\r
243   def concat(ar)\r
244     super(ar) if internal_will_accept?(true, *ar)\r
245   end\r
246 \r
247   # Overloaded from Array#insert, checks for appropriate class of argument\r
248   # elements\r
249   #\r
250   def insert(idx, *ar)\r
251     super(idx, *ar) if internal_will_accept?(true, *ar)\r
252   end\r
253 \r
254   # Overloaded from Array#replace, checks for appropriate class of argument\r
255   # elements\r
256   #\r
257   def replace(ar)\r
258     super(ar) if (ar.kind_of?(ArrayOf) && ar.element_class <= @element_class) or internal_will_accept?(true, *ar)\r
259   end\r
260 \r
261   # Overloaded from Array#push, checks for appropriate class of argument\r
262   # elements\r
263   #\r
264   def push(*ar)\r
265     super(*ar) if internal_will_accept?(true, *ar)\r
266   end\r
267 \r
268   # Overloaded from Array#unshift, checks for appropriate class of argument(s)\r
269   #\r
270   def unshift(*els)\r
271     els.each { |el|\r
272       super(el) if internal_will_accept?(true, *els)\r
273     }\r
274   end\r
275 \r
276   # Modifying methods which we don't handle yet are made private\r
277   #\r
278   private :[]=, :collect!, :map!, :fill, :flatten!\r
279 \r
280 end\r
281 \r
282 \r
283 # The Irc module is used to keep all IRC-related classes\r
284 # in the same namespace\r
285 #\r
286 module Irc\r
287 \r
288 \r
289   # A Netmask identifies each user by collecting its nick, username and\r
290   # hostname in the form <tt>nick!user@host</tt>\r
291   #\r
292   # Netmasks can also contain glob patterns in any of their components; in this\r
293   # form they are used to refer to more than a user or to a user appearing\r
294   # under different\r
295   # forms.\r
296   #\r
297   # Example:\r
298   # * <tt>*!*@*</tt> refers to everybody\r
299   # * <tt>*!someuser@somehost</tt> refers to user +someuser+ on host +somehost+\r
300   #   regardless of the nick used.\r
301   #\r
302   class Netmask\r
303     attr_reader :nick, :user, :host\r
304     attr_reader :casemap\r
305 \r
306     # call-seq:\r
307     #   Netmask.new(netmask) => new_netmask\r
308     #   Netmask.new(hash={}, casemap=nil) => new_netmask\r
309     #   Netmask.new("nick!user@host", casemap=nil) => new_netmask\r
310     #\r
311     # Create a new Netmask in any of these forms\r
312     # 1. from another Netmask (does a .dup)\r
313     # 2. from a Hash with any of the keys <tt>:nick</tt>, <tt>:user</tt> and\r
314     #    <tt>:host</tt>\r
315     # 3. from a String in the form <tt>nick!user@host</tt>\r
316     #\r
317     # In all but the first forms a casemap may be speficied, the default\r
318     # being 'rfc1459'.\r
319     #\r
320     # The nick is downcased following IRC rules and according to the given casemap.\r
321     #\r
322     # FIXME check if user and host need to be downcased too.\r
323     #\r
324     # Empty +nick+, +user+ or +host+ are converted to the generic glob pattern\r
325     #\r
326     def initialize(str={}, casemap=nil)\r
327       case str\r
328       when Netmask\r
329         raise ArgumentError, "Can't set casemap when initializing from other Netmask" if casemap\r
330         @casemap = str.casemap.dup\r
331         @nick = str.nick.dup\r
332         @user = str.user.dup\r
333         @host = str.host.dup\r
334       when Hash\r
335         @casemap = casemap || str[:casemap] || 'rfc1459'\r
336         @nick = str[:nick].to_s.irc_downcase(@casemap)\r
337         @user = str[:user].to_s\r
338         @host = str[:host].to_s\r
339       when String\r
340         case str\r
341         when ""\r
342           @casemap = casemap || 'rfc1459'\r
343           @nick = nil\r
344           @user = nil\r
345           @host = nil\r
346         when /^(\S+?)(?:!(\S+)@(?:(\S+))?)?$/\r
347           @casemap = casemap || 'rfc1459'\r
348           @nick = $1.irc_downcase(@casemap)\r
349           @user = $2\r
350           @host = $3\r
351         else\r
352           raise ArgumentError, "#{str} is not a valid netmask"\r
353         end\r
354       else\r
355         raise ArgumentError, "#{str} is not a valid netmask"\r
356       end\r
357 \r
358       @nick = "*" if @nick.to_s.empty?\r
359       @user = "*" if @user.to_s.empty?\r
360       @host = "*" if @host.to_s.empty?\r
361     end\r
362 \r
363     def inspect\r
364       str = "<#{self.class}:#{'0x%x' % self.object_id}:"\r
365       str << " @nick=#{@nick.inspect} @user=#{@user.inspect}"\r
366       str << " @host=#{@host.inspect}>"\r
367       str\r
368     end\r
369 \r
370     # Equality: two Netmasks are equal if they have the same @nick, @user, @host and @casemap\r
371     #\r
372     def ==(other)\r
373       self.class == other.class && @nick == other.nick && @user == other.user && @host == other.host && @casemap == other.casemap\r
374     end\r
375 \r
376     # This method changes the nick of the Netmask, downcasing the argument\r
377     # following IRC rules and defaulting to the generic glob pattern if\r
378     # the result is the null string.\r
379     #\r
380     def nick=(newnick)\r
381       @nick = newnick.to_s.irc_downcase(@casemap)\r
382       @nick = "*" if @nick.empty?\r
383     end\r
384 \r
385     # This method changes the user of the Netmask, defaulting to the generic\r
386     # glob pattern if the result is the null string.\r
387     #\r
388     def user=(newuser)\r
389       @user = newuser.to_s\r
390       @user = "*" if @user.empty?\r
391     end\r
392 \r
393     # This method changes the hostname of the Netmask, defaulting to the generic\r
394     # glob pattern if the result is the null string.\r
395     #\r
396     def host=(newhost)\r
397       @host = newhost.to_s\r
398       @host = "*" if @host.empty?\r
399     end\r
400 \r
401     # This method changes the casemap of a Netmask, which is needed in some\r
402     # extreme circumstances. Please use sparingly\r
403     #\r
404     def casemap=(newcmap)\r
405       @casemap = newcmap.to_s\r
406       @casemap = "rfc1459" if @casemap.empty?\r
407     end\r
408 \r
409     # This method checks if a Netmask is definite or not, by seeing if\r
410     # any of its components are defined by globs\r
411     #\r
412     def has_irc_glob?\r
413       return @nick.has_irc_glob? || @user.has_irc_glob? || @host.has_irc_glob?\r
414     end\r
415 \r
416     # A Netmask is easily converted to a String for the usual representation\r
417     # \r
418     def fullform\r
419       return "#{nick}!#{user}@#{host}"\r
420     end\r
421     alias :to_s :fullform\r
422 \r
423     # This method is used to match the current Netmask against another one\r
424     #\r
425     # The method returns true if each component of the receiver matches the\r
426     # corresponding component of the argument. By _matching_ here we mean that\r
427     # any netmask described by the receiver is also described by the argument.\r
428     #\r
429     # In this sense, matching is rather simple to define in the case when the\r
430     # receiver has no globs: it is just necessary to check if the argument\r
431     # describes the receiver, which can be done by matching it against the\r
432     # argument converted into an IRC Regexp (see String#to_irc_regexp).\r
433     #\r
434     # The situation is also easy when the receiver has globs and the argument\r
435     # doesn't, since in this case the result is false.\r
436     #\r
437     # The more complex case in which both the receiver and the argument have\r
438     # globs is not handled yet.\r
439     # \r
440     def matches?(arg)\r
441       cmp = Netmask.new(arg)\r
442       raise TypeError, "#{arg} and #{self} have different casemaps" if @casemap != cmp.casemap\r
443       raise TypeError, "#{arg} is not a valid Netmask" unless cmp.kind_of?(Netmask)\r
444       [:nick, :user, :host].each { |component|\r
445         us = self.send(component)\r
446         them = cmp.send(component)\r
447         raise NotImplementedError if us.has_irc_glob? && them.has_irc_glob?\r
448         return false if us.has_irc_glob? && !them.has_irc_glob?\r
449         return false unless us =~ them.to_irc_regexp\r
450       }\r
451       return true\r
452     end\r
453 \r
454     # Case equality. Checks if arg matches self\r
455     #\r
456     def ===(arg)\r
457       Netmask.new(arg).matches?(self)\r
458     end\r
459 \r
460     def <=>(arg)\r
461       case arg\r
462       when Netmask\r
463         self.fullform <=> arg.fullform\r
464       else\r
465         self.to_s <=> arg.to_s\r
466       end\r
467     end\r
468 \r
469   end\r
470 \r
471 \r
472   # A NetmaskList is an ArrayOf <code>Netmask</code>s\r
473   #\r
474   class NetmaskList < ArrayOf\r
475 \r
476     # Create a new NetmaskList, optionally filling it with the elements from\r
477     # the Array argument fed to it.\r
478     def initialize(ar=[])\r
479       super(Netmask, ar)\r
480     end\r
481   end\r
482 \r
483 \r
484   # An IRC User is identified by his/her Netmask (which must not have\r
485   # globs). In fact, User is just a subclass of Netmask. However,\r
486   # a User will not allow one's host or user data to be changed.\r
487   #\r
488   # Due to the idiosincrasies of the IRC protocol, we allow\r
489   # the creation of a user with an unknown mask represented by the\r
490   # glob pattern *@*. Only in this case they may be set.\r
491   #\r
492   # TODO list:\r
493   # * see if it's worth to add the other USER data\r
494   # * see if it's worth to add NICKSERV status\r
495   #\r
496   class User < Netmask\r
497     alias :to_s :nick\r
498 \r
499     # Create a new IRC User from a given Netmask (or anything that can be converted\r
500     # into a Netmask) provided that the given Netmask does not have globs.\r
501     #\r
502     def initialize(str="", casemap=nil)\r
503       super\r
504       raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if nick.has_irc_glob? && nick != "*"\r
505       raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if user.has_irc_glob? && user != "*"\r
506       raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if host.has_irc_glob? && host != "*"\r
507       @away = false\r
508     end\r
509 \r
510     # We only allow the user to be changed if it was "*". Otherwise,\r
511     # we raise an exception if the new host is different from the old one\r
512     #\r
513     def user=(newuser)\r
514       if user == "*"\r
515         super\r
516       else\r
517         raise "Can't change the username of user #{self}" if user != newuser\r
518       end\r
519     end\r
520 \r
521     # We only allow the host to be changed if it was "*". Otherwise,\r
522     # we raise an exception if the new host is different from the old one\r
523     #\r
524     def host=(newhost)\r
525       if host == "*"\r
526         super\r
527       else\r
528         raise "Can't change the hostname of user #{self}" if host != newhost \r
529       end\r
530     end\r
531 \r
532     # Checks if a User is well-known or not by looking at the hostname and user\r
533     #\r
534     def known?\r
535       return user!="*" && host!="*"\r
536     end\r
537 \r
538     # Is the user away?\r
539     #\r
540     def away?\r
541       return @away\r
542     end\r
543 \r
544     # Set the away status of the user. Use away=(nil) or away=(false)\r
545     # to unset away\r
546     #\r
547     def away=(msg="")\r
548       if msg\r
549         @away = msg\r
550       else\r
551         @away = false\r
552       end\r
553     end\r
554   end\r
555 \r
556 \r
557   # A UserList is an ArrayOf <code>User</code>s\r
558   #\r
559   class UserList < ArrayOf\r
560 \r
561     # Create a new UserList, optionally filling it with the elements from\r
562     # the Array argument fed to it.\r
563     def initialize(ar=[])\r
564       super(User, ar)\r
565     end\r
566   end\r
567 \r
568 \r
569   # A ChannelTopic represents the topic of a channel. It consists of\r
570   # the topic itself, who set it and when\r
571   class ChannelTopic\r
572     attr_accessor :text, :set_by, :set_on\r
573     alias :to_s :text\r
574 \r
575     # Create a new ChannelTopic setting the text, the creator and\r
576     # the creation time\r
577     def initialize(text="", set_by="", set_on=Time.new)\r
578       @text = text\r
579       @set_by = set_by\r
580       @set_on = Time.new\r
581     end\r
582 \r
583     # Replace a ChannelTopic with another one\r
584     def replace(topic)\r
585       raise TypeError, "#{topic.inspect} is not an Irc::ChannelTopic" unless topic.kind_of?(ChannelTopic)\r
586       @text = topic.text.dup\r
587       @set_by = topic.set_by.dup\r
588       @set_on = topic.set_on.dup\r
589     end\r
590   end\r
591 \r
592 \r
593   # Mode on a channel\r
594   class ChannelMode\r
595     def initialize(ch)\r
596       @channel = ch\r
597     end\r
598   end\r
599 \r
600 \r
601   # Channel modes of type A manipulate lists\r
602   #\r
603   class ChannelModeTypeA < ChannelMode\r
604     def initialize(ch)\r
605       super\r
606       @list = NetmaskList.new\r
607     end\r
608 \r
609     def set(val)\r
610       nm = @channel.server.new_netmask(val)\r
611       @list << nm unless @list.include?(nm)\r
612     end\r
613 \r
614     def reset(val)\r
615       nm = @channel.server.new_netmask(val)\r
616       @list.delete(nm)\r
617     end\r
618   end\r
619 \r
620   # Channel modes of type B need an argument\r
621   #\r
622   class ChannelModeTypeB < ChannelMode\r
623     def initialize(ch)\r
624       super\r
625       @arg = nil\r
626     end\r
627 \r
628     def set(val)\r
629       @arg = val\r
630     end\r
631 \r
632     def reset(val)\r
633       @arg = nil if @arg == val\r
634     end\r
635   end\r
636 \r
637   # Channel modes that change the User prefixes are like\r
638   # Channel modes of type B, except that they manipulate\r
639   # lists of Users, so they are somewhat similar to channel\r
640   # modes of type A\r
641   #\r
642   class ChannelUserMode < ChannelModeTypeB\r
643     def initialize(ch)\r
644       super\r
645       @list = UserList.new\r
646     end\r
647 \r
648     def set(val)\r
649       u = @channel.server.user(val)\r
650       @list << u unless @list.include?(u)\r
651     end\r
652 \r
653     def reset(val)\r
654       u = @channel.server.user(val)\r
655       @list.delete(u)\r
656     end\r
657   end\r
658 \r
659   # Channel modes of type C need an argument when set,\r
660   # but not when they get reset\r
661   #\r
662   class ChannelModeTypeC < ChannelMode\r
663     def initialize(ch)\r
664       super\r
665       @arg = false\r
666     end\r
667 \r
668     def set(val)\r
669       @arg = val\r
670     end\r
671 \r
672     def reset\r
673       @arg = false\r
674     end\r
675   end\r
676 \r
677   # Channel modes of type D are basically booleans\r
678   class ChannelModeTypeD < ChannelMode\r
679     def initialize(ch)\r
680       super\r
681       @set = false\r
682     end\r
683 \r
684     def set?\r
685       return @set\r
686     end\r
687 \r
688     def set\r
689       @set = true\r
690     end\r
691 \r
692     def reset\r
693       @set = false\r
694     end\r
695   end\r
696 \r
697 \r
698   # An IRC Channel is identified by its name, and it has a set of properties:\r
699   # * a topic\r
700   # * a UserList\r
701   # * a set of modes\r
702   #\r
703   class Channel\r
704     attr_reader :name, :topic, :mode, :users, :server\r
705     alias :to_s :name\r
706 \r
707     def inspect\r
708       str = "<#{self.class}:#{'0x%x' % self.object_id}:"\r
709       str << " on server #{server}"\r
710       str << " @name=#{@name.inspect} @topic=#{@topic.text.inspect}"\r
711       str << " @users=<#{@users.sort.join(', ')}>"\r
712       str\r
713     end\r
714 \r
715     # Creates a new channel with the given name, optionally setting the topic\r
716     # and an initial users list.\r
717     #\r
718     # No additional info is created here, because the channel flags and userlists\r
719     # allowed depend on the server.\r
720     #\r
721     # FIXME doesn't check if users have the same casemap as the channel yet\r
722     #\r
723     def initialize(server, name, topic=nil, users=[])\r
724       raise TypeError, "First parameter must be an Irc::Server" unless server.kind_of?(Server)\r
725       raise ArgumentError, "Channel name cannot be empty" if name.to_s.empty?\r
726       raise ArgumentError, "Unknown channel prefix #{name[0].chr}" if name !~ /^[&#+!]/\r
727       raise ArgumentError, "Invalid character in #{name.inspect}" if name =~ /[ \x07,]/\r
728 \r
729       @server = server\r
730 \r
731       @name = name.irc_downcase(casemap)\r
732 \r
733       @topic = topic || ChannelTopic.new\r
734 \r
735       case users\r
736       when UserList\r
737         @users = users\r
738       when Array\r
739         @users = UserList.new(users)\r
740       else\r
741         raise ArgumentError, "Invalid user list #{users.inspect}"\r
742       end\r
743 \r
744       # Flags\r
745       @mode = {}\r
746     end\r
747 \r
748     # Returns the casemap of the originating server\r
749     def casemap\r
750       return @server.casemap\r
751     end\r
752 \r
753     # Removes a user from the channel\r
754     #\r
755     def delete_user(user)\r
756       @mode.each { |sym, mode|\r
757         mode.reset(user) if mode.kind_of?(ChannelUserMode)\r
758       }\r
759       @users.delete(user)\r
760     end\r
761 \r
762     # The channel prefix\r
763     #\r
764     def prefix\r
765       name[0].chr\r
766     end\r
767 \r
768     # A channel is local to a server if it has the '&' prefix\r
769     #\r
770     def local?\r
771       name[0] = 0x26\r
772     end\r
773 \r
774     # A channel is modeless if it has the '+' prefix\r
775     #\r
776     def modeless?\r
777       name[0] = 0x2b\r
778     end\r
779 \r
780     # A channel is safe if it has the '!' prefix\r
781     #\r
782     def safe?\r
783       name[0] = 0x21\r
784     end\r
785 \r
786     # A channel is safe if it has the '#' prefix\r
787     #\r
788     def normal?\r
789       name[0] = 0x23\r
790     end\r
791 \r
792     # Create a new mode\r
793     #\r
794     def create_mode(sym, kl)\r
795       @mode[sym.to_sym] = kl.new(self)\r
796     end\r
797   end\r
798 \r
799 \r
800   # A ChannelList is an ArrayOf <code>Channel</code>s\r
801   #\r
802   class ChannelList < ArrayOf\r
803 \r
804     # Create a new ChannelList, optionally filling it with the elements from\r
805     # the Array argument fed to it.\r
806     def initialize(ar=[])\r
807       super(Channel, ar)\r
808     end\r
809   end\r
810 \r
811 \r
812   # An IRC Server represents the Server the client is connected to.\r
813   #\r
814   class Server\r
815 \r
816     attr_reader :hostname, :version, :usermodes, :chanmodes\r
817     alias :to_s :hostname\r
818     attr_reader :supports, :capabilities\r
819 \r
820     attr_reader :channels, :users\r
821 \r
822     def channel_names\r
823       @channels.map { |ch| ch.name }\r
824     end\r
825 \r
826     def user_nicks\r
827       @users.map { |u| u.nick }\r
828     end\r
829 \r
830     def inspect\r
831       chans = @channels.map { |ch|\r
832         ch.inspect\r
833       }\r
834       users = @users.map { |u|\r
835         u.inspect\r
836       }.sort\r
837 \r
838       str = "<#{self.class}:#{'0x%x' % self.object_id}:"\r
839       str << " @channels=#{chans}"\r
840       str << " @users=#{users}>"\r
841       str\r
842     end\r
843 \r
844     # Create a new Server, with all instance variables reset\r
845     # to nil (for scalar variables), the channel and user lists\r
846     # are empty, and @supports is initialized to the default values\r
847     # for all known supported features.\r
848     #\r
849     def initialize\r
850       @hostname = @version = @usermodes = @chanmodes = nil\r
851 \r
852       @channels = ChannelList.new\r
853 \r
854       @users = UserList.new\r
855 \r
856       reset_capabilities\r
857     end\r
858 \r
859     # Resets the server capabilities\r
860     #\r
861     def reset_capabilities\r
862       @supports = {\r
863         :casemapping => 'rfc1459',\r
864         :chanlimit => {},\r
865         :chanmodes => {\r
866           :typea => nil, # Type A: address lists\r
867           :typeb => nil, # Type B: needs a parameter\r
868           :typec => nil, # Type C: needs a parameter when set\r
869           :typed => nil  # Type D: must not have a parameter\r
870         },\r
871         :channellen => 200,\r
872         :chantypes => "#&",\r
873         :excepts => nil,\r
874         :idchan => {},\r
875         :invex => nil,\r
876         :kicklen => nil,\r
877         :maxlist => {},\r
878         :modes => 3,\r
879         :network => nil,\r
880         :nicklen => 9,\r
881         :prefix => {\r
882           :modes => 'ov'.scan(/./),\r
883           :prefixes => '@+'.scan(/./)\r
884         },\r
885         :safelist => nil,\r
886         :statusmsg => nil,\r
887         :std => nil,\r
888         :targmax => {},\r
889         :topiclen => nil\r
890       }\r
891       @capabilities = {}\r
892     end\r
893 \r
894     # Resets the Channel and User list\r
895     #\r
896     def reset_lists\r
897       @users.each { |u|\r
898         delete_user(u)\r
899       }\r
900       @channels.each { |u|\r
901         delete_channel(u)\r
902       }\r
903     end\r
904 \r
905     # Clears the server\r
906     #\r
907     def clear\r
908       reset_lists\r
909       reset_capabilities\r
910     end\r
911 \r
912     # This method is used to parse a 004 RPL_MY_INFO line\r
913     #\r
914     def parse_my_info(line)\r
915       ar = line.split(' ')\r
916       @hostname = ar[0]\r
917       @version = ar[1]\r
918       @usermodes = ar[2]\r
919       @chanmodes = ar[3]\r
920     end\r
921 \r
922     def noval_warn(key, val, &block)\r
923       if val\r
924         yield if block_given?\r
925       else\r
926         warn "No #{key.to_s.upcase} value"\r
927       end\r
928     end\r
929 \r
930     def val_warn(key, val, &block)\r
931       if val == true or val == false or val.nil?\r
932         yield if block_given?\r
933       else\r
934         warn "No #{key.to_s.upcase} value must be specified, got #{val}"\r
935       end\r
936     end\r
937     private :noval_warn, :val_warn\r
938 \r
939     # This method is used to parse a 005 RPL_ISUPPORT line\r
940     #\r
941     # See the RPL_ISUPPORT draft[http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt]\r
942     #\r
943     def parse_isupport(line)\r
944       debug "Parsing ISUPPORT #{line.inspect}"\r
945       ar = line.split(' ')\r
946       reparse = ""\r
947       ar.each { |en|\r
948         prekey, val = en.split('=', 2)\r
949         if prekey =~ /^-(.*)/\r
950           key = $1.downcase.to_sym\r
951           val = false\r
952         else\r
953           key = prekey.downcase.to_sym\r
954         end\r
955         case key\r
956         when :casemapping, :network\r
957           noval_warn(key, val) {\r
958             @supports[key] = val\r
959             @users.each { |u|\r
960               debug "Resetting casemap of #{u} from #{u.casemap} to #{val}"\r
961               u.casemap = val\r
962             }\r
963           }\r
964         when :chanlimit, :idchan, :maxlist, :targmax\r
965           noval_warn(key, val) {\r
966             groups = val.split(',')\r
967             groups.each { |g|\r
968               k, v = g.split(':')\r
969               @supports[key][k] = v.to_i\r
970             }\r
971           }\r
972         when :maxchannels\r
973           noval_warn(key, val) {\r
974             reparse += "CHANLIMIT=(chantypes):#{val} "\r
975           }\r
976         when :maxtargets\r
977           noval_warn(key, val) {\r
978             @supports[key]['PRIVMSG'] = val.to_i\r
979             @supports[key]['NOTICE'] = val.to_i\r
980           }\r
981         when :chanmodes\r
982           noval_warn(key, val) {\r
983             groups = val.split(',')\r
984             @supports[key][:typea] = groups[0].scan(/./).map { |x| x.to_sym}\r
985             @supports[key][:typeb] = groups[1].scan(/./).map { |x| x.to_sym}\r
986             @supports[key][:typec] = groups[2].scan(/./).map { |x| x.to_sym}\r
987             @supports[key][:typed] = groups[3].scan(/./).map { |x| x.to_sym}\r
988           }\r
989         when :channellen, :kicklen, :modes, :topiclen\r
990           if val\r
991             @supports[key] = val.to_i\r
992           else\r
993             @supports[key] = nil\r
994           end\r
995         when :chantypes\r
996           @supports[key] = val # can also be nil\r
997         when :excepts\r
998           val ||= 'e'\r
999           @supports[key] = val\r
1000         when :invex\r
1001           val ||= 'I'\r
1002           @supports[key] = val\r
1003         when :nicklen\r
1004           noval_warn(key, val) {\r
1005             @supports[key] = val.to_i\r
1006           }\r
1007         when :prefix\r
1008           if val\r
1009             val.scan(/\((.*)\)(.*)/) { |m, p|\r
1010               @supports[key][:modes] = m.scan(/./).map { |x| x.to_sym}\r
1011               @supports[key][:prefixes] = p.scan(/./).map { |x| x.to_sym}\r
1012             }\r
1013           else\r
1014             @supports[key][:modes] = nil\r
1015             @supports[key][:prefixes] = nil\r
1016           end\r
1017         when :safelist\r
1018           val_warn(key, val) {\r
1019             @supports[key] = val.nil? ? true : val\r
1020           }\r
1021         when :statusmsg\r
1022           noval_warn(key, val) {\r
1023             @supports[key] = val.scan(/./)\r
1024           }\r
1025         when :std\r
1026           noval_warn(key, val) {\r
1027             @supports[key] = val.split(',')\r
1028           }\r
1029         else\r
1030           @supports[key] =  val.nil? ? true : val\r
1031         end\r
1032       }\r
1033       reparse.gsub!("(chantypes)",@supports[:chantypes])\r
1034       parse_isupport(reparse) unless reparse.empty?\r
1035     end\r
1036 \r
1037     # Returns the casemap of the server.\r
1038     #\r
1039     def casemap\r
1040       @supports[:casemapping] || 'rfc1459'\r
1041     end\r
1042 \r
1043     # Returns User or Channel depending on what _name_ can be\r
1044     # a name of\r
1045     #\r
1046     def user_or_channel?(name)\r
1047       if supports[:chantypes].include?(name[0])\r
1048         return Channel\r
1049       else\r
1050         return User\r
1051       end\r
1052     end\r
1053 \r
1054     # Returns the actual User or Channel object matching _name_\r
1055     #\r
1056     def user_or_channel(name)\r
1057       if supports[:chantypes].include?(name[0])\r
1058         return channel(name)\r
1059       else\r
1060         return user(name)\r
1061       end\r
1062     end\r
1063 \r
1064     # Checks if the receiver already has a channel with the given _name_\r
1065     #\r
1066     def has_channel?(name)\r
1067       channel_names.index(name.to_s)\r
1068     end\r
1069     alias :has_chan? :has_channel?\r
1070 \r
1071     # Returns the channel with name _name_, if available\r
1072     #\r
1073     def get_channel(name)\r
1074       idx = channel_names.index(name.to_s)\r
1075       channels[idx] if idx\r
1076     end\r
1077     alias :get_chan :get_channel\r
1078 \r
1079     # Create a new Channel object and add it to the list of\r
1080     # <code>Channel</code>s on the receiver, unless the channel\r
1081     # was present already. In this case, the default action is\r
1082     # to raise an exception, unless _fails_ is set to false\r
1083     #\r
1084     # The Channel is automatically created with the appropriate casemap\r
1085     #\r
1086     def new_channel(name, topic=nil, users=[], fails=true)\r
1087       ex = get_chan(name)\r
1088       if ex\r
1089         raise "Channel #{name} already exists on server #{self}" if fails\r
1090         return ex\r
1091       else\r
1092 \r
1093         prefix = name[0].chr\r
1094 \r
1095         # Give a warning if the new Channel goes over some server limits.\r
1096         #\r
1097         # FIXME might need to raise an exception\r
1098         #\r
1099         warn "#{self} doesn't support channel prefix #{prefix}" unless @supports[:chantypes].include?(prefix)\r
1100         warn "#{self} doesn't support channel names this long (#{name.length} > #{@supports[:channellen]})" unless name.length <= @supports[:channellen]\r
1101 \r
1102         # Next, we check if we hit the limit for channels of type +prefix+\r
1103         # if the server supports +chanlimit+\r
1104         #\r
1105         @supports[:chanlimit].keys.each { |k|\r
1106           next unless k.include?(prefix)\r
1107           count = 0\r
1108           channel_names.each { |n|\r
1109             count += 1 if k.include?(n[0])\r
1110           }\r
1111           raise IndexError, "Already joined #{count} channels with prefix #{k}" if count == @supports[:chanlimit][k]\r
1112         }\r
1113 \r
1114         # So far, everything is fine. Now create the actual Channel\r
1115         #\r
1116         chan = Channel.new(self, name, topic, users)\r
1117 \r
1118         # We wade through +prefix+ and +chanmodes+ to create appropriate\r
1119         # lists and flags for this channel\r
1120 \r
1121         @supports[:prefix][:modes].each { |mode|\r
1122           chan.create_mode(mode, ChannelUserMode)\r
1123         } if @supports[:prefix][:modes]\r
1124 \r
1125         @supports[:chanmodes].each { |k, val|\r
1126           if val\r
1127             case k\r
1128             when :typea\r
1129               val.each { |mode|\r
1130                 chan.create_mode(mode, ChannelModeTypeA)\r
1131               }\r
1132             when :typeb\r
1133               val.each { |mode|\r
1134                 chan.create_mode(mode, ChannelModeTypeB)\r
1135               }\r
1136             when :typec\r
1137               val.each { |mode|\r
1138                 chan.create_mode(mode, ChannelModeTypeC)\r
1139               }\r
1140             when :typed\r
1141               val.each { |mode|\r
1142                 chan.create_mode(mode, ChannelModeTypeD)\r
1143               }\r
1144             end\r
1145           end\r
1146         }\r
1147 \r
1148         @channels << chan\r
1149         # debug "Created channel #{chan.inspect}"\r
1150         return chan\r
1151       end\r
1152     end\r
1153 \r
1154     # Returns the Channel with the given _name_ on the server,\r
1155     # creating it if necessary. This is a short form for\r
1156     # new_channel(_str_, nil, [], +false+)\r
1157     #\r
1158     def channel(str)\r
1159       new_channel(str,nil,[],false)\r
1160     end\r
1161 \r
1162     # Remove Channel _name_ from the list of <code>Channel</code>s\r
1163     #\r
1164     def delete_channel(name)\r
1165       idx = has_channel?(name)\r
1166       raise "Tried to remove unmanaged channel #{name}" unless idx\r
1167       @channels.delete_at(idx)\r
1168     end\r
1169 \r
1170     # Checks if the receiver already has a user with the given _nick_\r
1171     #\r
1172     def has_user?(nick)\r
1173       user_nicks.index(nick.to_s)\r
1174     end\r
1175 \r
1176     # Returns the user with nick _nick_, if available\r
1177     #\r
1178     def get_user(nick)\r
1179       idx = user_nicks.index(nick.to_s)\r
1180       @users[idx] if idx\r
1181     end\r
1182 \r
1183     # Create a new User object and add it to the list of\r
1184     # <code>User</code>s on the receiver, unless the User\r
1185     # was present already. In this case, the default action is\r
1186     # to raise an exception, unless _fails_ is set to false\r
1187     #\r
1188     # The User is automatically created with the appropriate casemap\r
1189     #\r
1190     def new_user(str, fails=true)\r
1191       case str\r
1192       when User\r
1193         tmp = str\r
1194       else\r
1195         tmp = User.new(str, self.casemap)\r
1196       end\r
1197       # debug "Creating or selecting user #{tmp.inspect} from #{str.inspect}"\r
1198       old = get_user(tmp.nick)\r
1199       if old\r
1200         # debug "User already existed as #{old.inspect}"\r
1201         if tmp.known?\r
1202           if old.known?\r
1203             # Do not raise an error: things like Freenode change the hostname after identification\r
1204             warning "User #{tmp.nick} has inconsistent Netmasks! #{self} knows #{old.inspect} but access was tried with #{tmp.inspect}" if old != tmp\r
1205             raise "User #{tmp} already exists on server #{self}" if fails\r
1206           end\r
1207           if old != tmp\r
1208             old.user = tmp.user\r
1209             old.host = tmp.host\r
1210             # debug "User improved to #{old.inspect}"\r
1211           end\r
1212         end\r
1213         return old\r
1214       else\r
1215         warn "#{self} doesn't support nicknames this long (#{tmp.nick.length} > #{@supports[:nicklen]})" unless tmp.nick.length <= @supports[:nicklen]\r
1216         @users << tmp\r
1217         return @users.last\r
1218       end\r
1219     end\r
1220 \r
1221     # Returns the User with the given Netmask on the server,\r
1222     # creating it if necessary. This is a short form for\r
1223     # new_user(_str_, +false+)\r
1224     #\r
1225     def user(str)\r
1226       u = new_user(str, false)\r
1227       debug "Server user #{u.inspect} from #{str.inspect}"\r
1228       u\r
1229     end\r
1230 \r
1231     # Remove User _someuser_ from the list of <code>User</code>s.\r
1232     # _someuser_ must be specified with the full Netmask.\r
1233     #\r
1234     def delete_user(someuser)\r
1235       idx = has_user?(someuser.nick)\r
1236       raise "Tried to remove unmanaged user #{user}" unless idx\r
1237       have = self.user(someuser)\r
1238       raise "User #{someuser.nick} has inconsistent Netmasks! #{self} knows #{have} but access was tried with #{someuser}" if have != someuser && have.user != "*" && have.host != "*"\r
1239       @channels.each { |ch|\r
1240         delete_user_from_channel(have, ch)\r
1241       }\r
1242       @users.delete_at(idx)\r
1243     end\r
1244 \r
1245     # Create a new Netmask object with the appropriate casemap\r
1246     #\r
1247     def new_netmask(str)\r
1248       if str.kind_of?(Netmask )\r
1249         raise "Wrong casemap for Netmask #{str.inspect}" if str.casemap != self.casemap\r
1250         return str\r
1251       end\r
1252       Netmask.new(str, self.casemap)\r
1253     end\r
1254 \r
1255     # Finds all <code>User</code>s on server whose Netmask matches _mask_\r
1256     #\r
1257     def find_users(mask)\r
1258       nm = new_netmask(mask)\r
1259       @users.inject(UserList.new) {\r
1260         |list, user|\r
1261         if user.user == "*" or user.host == "*"\r
1262           list << user if user.nick =~ nm.nick.to_irc_regexp\r
1263         else\r
1264           list << user if user.matches?(nm)\r
1265         end\r
1266         list\r
1267       }\r
1268     end\r
1269 \r
1270     # Deletes User from Channel\r
1271     #\r
1272     def delete_user_from_channel(user, channel)\r
1273       channel.delete_user(user)\r
1274     end\r
1275 \r
1276   end\r
1277 \r
1278 end\r
1279 \r