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