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