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