New Irc framework: fix channel modes handling
[rbot] / lib / rbot / irc.rb
1 #-- vim:sw=2:et\r
2 # General TODO list\r
3 # * do we want to handle a Channel list for each User telling which\r
4 #   Channels is the User on (of those the client is on too)?\r
5 #   We may want this so that when a User leaves all Channels and he hasn't\r
6 #   sent us privmsgs, we know remove him from the Server @users list\r
7 #++\r
8 # :title: IRC module\r
9 #\r
10 # Basic IRC stuff\r
11 #\r
12 # This module defines the fundamental building blocks for IRC\r
13 #\r
14 # Author:: Giuseppe Bilotta (giuseppe.bilotta@gmail.com)\r
15 # Copyright:: Copyright (c) 2006 Giuseppe Bilotta\r
16 # License:: GPLv2\r
17 \r
18 \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, "#{el.inspect} is not of class #{@element_class}" 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 \r
489     # Replace a ChannelTopic with another one\r
490     def replace(topic)\r
491       raise TypeError, "#{topic.inspect} is not an Irc::ChannelTopic" unless topic.class <= ChannelTopic\r
492       @text = topic.text.dup\r
493       @set_by = topic.set_by.dup\r
494       @set_on = topic.set_on.dup\r
495     end\r
496   end\r
497 \r
498 \r
499   # Mode on a channel\r
500   class ChannelMode\r
501     def initialize(ch)\r
502       @channel = ch\r
503     end\r
504   end\r
505 \r
506 \r
507   # Channel modes of type A manipulate lists\r
508   #\r
509   class ChannelModeTypeA < ChannelMode\r
510     def initialize(ch)\r
511       super\r
512       @list = NetmaskList.new\r
513     end\r
514 \r
515     def set(val)\r
516       nm = @channel.server.new_netmask(val)\r
517       @list << nm unless @list.include?(nm)\r
518     end\r
519 \r
520     def reset(val)\r
521       nm = @channel.server.new_netmask(val)\r
522       @list.delete(nm)\r
523     end\r
524   end\r
525 \r
526   # Channel modes of type B need an argument\r
527   #\r
528   class ChannelModeTypeB < ChannelMode\r
529     def initialize(ch)\r
530       super\r
531       @arg = nil\r
532     end\r
533 \r
534     def set(val)\r
535       @arg = val\r
536     end\r
537 \r
538     def reset(val)\r
539       @arg = nil if @arg == val\r
540     end\r
541   end\r
542 \r
543   # Channel modes that change the User prefixes are like\r
544   # Channel modes of type B, except that they manipulate\r
545   # lists of Users, so they are somewhat similar to channel\r
546   # modes of type A\r
547   #\r
548   class ChannelUserMode < ChannelModeTypeB\r
549     def initialize(ch)\r
550       super\r
551       @list = UserList.new\r
552     end\r
553 \r
554     def set(val)\r
555       u = @channel.server.user(val)\r
556       @list << u unless @list.include?(u)\r
557     end\r
558 \r
559     def reset(val)\r
560       u = @channel.server.user(val)\r
561       @list.delete(u)\r
562     end\r
563   end\r
564 \r
565   # Channel modes of type C need an argument when set,\r
566   # but not when they get reset\r
567   #\r
568   class ChannelModeTypeC < ChannelMode\r
569     def initialize(ch)\r
570       super\r
571       @arg = false\r
572     end\r
573 \r
574     def set(val)\r
575       @arg = val\r
576     end\r
577 \r
578     def reset\r
579       @arg = false\r
580     end\r
581   end\r
582 \r
583   # Channel modes of type D are basically booleans\r
584   class ChannelModeTypeD < ChannelMode\r
585     def initialize(ch)\r
586       super\r
587       @set = false\r
588     end\r
589 \r
590     def set?\r
591       return @set\r
592     end\r
593 \r
594     def set\r
595       @set = true\r
596     end\r
597 \r
598     def reset\r
599       @set = false\r
600     end\r
601   end\r
602 \r
603 \r
604   # An IRC Channel is identified by its name, and it has a set of properties:\r
605   # * a topic\r
606   # * a UserList\r
607   # * a set of modes\r
608   #\r
609   class Channel\r
610     attr_reader :name, :topic, :mode, :users, :server\r
611     alias :to_s :name\r
612 \r
613     # A String describing the Channel and (some of its) internals\r
614     #\r
615     def inspect\r
616       str = "<#{self.class}:#{'0x%08x' % self.object_id}:"\r
617       str << " on server #{server}"\r
618       str << " @name=#{@name.inspect} @topic=#{@topic.text.inspect}"\r
619       str << " @users=<#{@users.join(', ')}>"\r
620       str\r
621     end\r
622 \r
623     # Creates a new channel with the given name, optionally setting the topic\r
624     # and an initial users list.\r
625     #\r
626     # No additional info is created here, because the channel flags and userlists\r
627     # allowed depend on the server.\r
628     #\r
629     # FIXME doesn't check if users have the same casemap as the channel yet\r
630     #\r
631     def initialize(server, name, topic=nil, users=[])\r
632       raise TypeError, "First parameter must be an Irc::Server" unless server.class <= Server\r
633       raise ArgumentError, "Channel name cannot be empty" if name.to_s.empty?\r
634       raise ArgumentError, "Unknown channel prefix #{name[0].chr}" if name !~ /^[&#+!]/\r
635       raise ArgumentError, "Invalid character in #{name.inspect}" if name =~ /[ \x07,]/\r
636 \r
637       @server = server\r
638 \r
639       @name = name.irc_downcase(casemap)\r
640 \r
641       @topic = topic || ChannelTopic.new\r
642 \r
643       case users\r
644       when UserList\r
645         @users = users\r
646       when Array\r
647         @users = UserList.new(users)\r
648       else\r
649         raise ArgumentError, "Invalid user list #{users.inspect}"\r
650       end\r
651 \r
652       # Flags\r
653       @mode = {}\r
654     end\r
655 \r
656     # Returns the casemap of the originating server\r
657     def casemap\r
658       return @server.casemap\r
659     end\r
660 \r
661     # Removes a user from the channel\r
662     #\r
663     def delete_user(user)\r
664       @mode.each { |sym, mode|\r
665         mode.reset(user) if mode.class <= ChannelUserMode\r
666       }\r
667       @users.delete(user)\r
668     end\r
669 \r
670     # The channel prefix\r
671     #\r
672     def prefix\r
673       name[0].chr\r
674     end\r
675 \r
676     # A channel is local to a server if it has the '&' prefix\r
677     #\r
678     def local?\r
679       name[0] = 0x26\r
680     end\r
681 \r
682     # A channel is modeless if it has the '+' prefix\r
683     #\r
684     def modeless?\r
685       name[0] = 0x2b\r
686     end\r
687 \r
688     # A channel is safe if it has the '!' prefix\r
689     #\r
690     def safe?\r
691       name[0] = 0x21\r
692     end\r
693 \r
694     # A channel is safe if it has the '#' prefix\r
695     #\r
696     def normal?\r
697       name[0] = 0x23\r
698     end\r
699 \r
700     # Create a new mode\r
701     #\r
702     def create_mode(sym, kl)\r
703       @mode[sym.to_sym] = kl.new(self)\r
704     end\r
705   end\r
706 \r
707 \r
708   # A ChannelList is an ArrayOf <code>Channel</code>s\r
709   #\r
710   class ChannelList < ArrayOf\r
711 \r
712     # Create a new ChannelList, optionally filling it with the elements from\r
713     # the Array argument fed to it.\r
714     def initialize(ar=[])\r
715       super(Channel, ar)\r
716     end\r
717   end\r
718 \r
719 \r
720   # An IRC Server represents the Server the client is connected to.\r
721   #\r
722   class Server\r
723 \r
724     attr_reader :hostname, :version, :usermodes, :chanmodes\r
725     alias :to_s :hostname\r
726     attr_reader :supports, :capabilities\r
727 \r
728     attr_reader :channels, :users\r
729 \r
730     # Create a new Server, with all instance variables reset\r
731     # to nil (for scalar variables), the channel and user lists\r
732     # are empty, and @supports is initialized to the default values\r
733     # for all known supported features.\r
734     #\r
735     def initialize\r
736       @hostname = @version = @usermodes = @chanmodes = nil\r
737 \r
738       @channels = ChannelList.new\r
739       @channel_names = Array.new\r
740 \r
741       @users = UserList.new\r
742       @user_nicks = Array.new\r
743 \r
744       reset_capabilities\r
745     end\r
746 \r
747     # Resets the server capabilities\r
748     #\r
749     def reset_capabilities\r
750       @supports = {\r
751         :casemapping => 'rfc1459',\r
752         :chanlimit => {},\r
753         :chanmodes => {\r
754           :typea => nil, # Type A: address lists\r
755           :typeb => nil, # Type B: needs a parameter\r
756           :typec => nil, # Type C: needs a parameter when set\r
757           :typed => nil  # Type D: must not have a parameter\r
758         },\r
759         :channellen => 200,\r
760         :chantypes => "#&",\r
761         :excepts => nil,\r
762         :idchan => {},\r
763         :invex => nil,\r
764         :kicklen => nil,\r
765         :maxlist => {},\r
766         :modes => 3,\r
767         :network => nil,\r
768         :nicklen => 9,\r
769         :prefix => {\r
770           :modes => 'ov'.scan(/./),\r
771           :prefixes => '@+'.scan(/./)\r
772         },\r
773         :safelist => nil,\r
774         :statusmsg => nil,\r
775         :std => nil,\r
776         :targmax => {},\r
777         :topiclen => nil\r
778       }\r
779       @capabilities = {}\r
780     end\r
781 \r
782     # Resets the Channel and User list\r
783     #\r
784     def reset_lists\r
785       @users.each { |u|\r
786         delete_user(u)\r
787       }\r
788       @channels.each { |u|\r
789         delete_channel(u)\r
790       }\r
791     end\r
792 \r
793     # Clears the server\r
794     #\r
795     def clear\r
796       reset_lists\r
797       reset_capabilities\r
798     end\r
799 \r
800     # This method is used to parse a 004 RPL_MY_INFO line\r
801     #\r
802     def parse_my_info(line)\r
803       ar = line.split(' ')\r
804       @hostname = ar[0]\r
805       @version = ar[1]\r
806       @usermodes = ar[2]\r
807       @chanmodes = ar[3]\r
808     end\r
809 \r
810     def noval_warn(key, val, &block)\r
811       if val\r
812         yield if block_given?\r
813       else\r
814         warn "No #{key.to_s.upcase} value"\r
815       end\r
816     end\r
817 \r
818     def val_warn(key, val, &block)\r
819       if val == true or val == false or val.nil?\r
820         yield if block_given?\r
821       else\r
822         warn "No #{key.to_s.upcase} value must be specified, got #{val}"\r
823       end\r
824     end\r
825     private :noval_warn, :val_warn\r
826 \r
827     # This method is used to parse a 005 RPL_ISUPPORT line\r
828     #\r
829     # See the RPL_ISUPPORT draft[http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt]\r
830     #\r
831     def parse_isupport(line)\r
832       debug "Parsing ISUPPORT #{line.inspect}"\r
833       ar = line.split(' ')\r
834       reparse = ""\r
835       ar.each { |en|\r
836         prekey, val = en.split('=', 2)\r
837         if prekey =~ /^-(.*)/\r
838           key = $1.downcase.to_sym\r
839           val = false\r
840         else\r
841           key = prekey.downcase.to_sym\r
842         end\r
843         case key\r
844         when :casemapping, :network\r
845           noval_warn(key, val) {\r
846             @supports[key] = val\r
847           }\r
848         when :chanlimit, :idchan, :maxlist, :targmax\r
849           noval_warn(key, val) {\r
850             groups = val.split(',')\r
851             groups.each { |g|\r
852               k, v = g.split(':')\r
853               @supports[key][k] = v.to_i\r
854             }\r
855           }\r
856         when :maxchannels\r
857           noval_warn(key, val) {\r
858             reparse += "CHANLIMIT=(chantypes):#{val} "\r
859           }\r
860         when :maxtargets\r
861           noval_warn(key, val) {\r
862             @supports[key]['PRIVMSG'] = val.to_i\r
863             @supports[key]['NOTICE'] = val.to_i\r
864           }\r
865         when :chanmodes\r
866           noval_warn(key, val) {\r
867             groups = val.split(',')\r
868             @supports[key][:typea] = groups[0].scan(/./).map { |x| x.to_sym}\r
869             @supports[key][:typeb] = groups[1].scan(/./).map { |x| x.to_sym}\r
870             @supports[key][:typec] = groups[2].scan(/./).map { |x| x.to_sym}\r
871             @supports[key][:typed] = groups[3].scan(/./).map { |x| x.to_sym}\r
872           }\r
873         when :channellen, :kicklen, :modes, :topiclen\r
874           if val\r
875             @supports[key] = val.to_i\r
876           else\r
877             @supports[key] = nil\r
878           end\r
879         when :chantypes\r
880           @supports[key] = val # can also be nil\r
881         when :excepts\r
882           val ||= 'e'\r
883           @supports[key] = val\r
884         when :invex\r
885           val ||= 'I'\r
886           @supports[key] = val\r
887         when :nicklen\r
888           noval_warn(key, val) {\r
889             @supports[key] = val.to_i\r
890           }\r
891         when :prefix\r
892           if val\r
893             val.scan(/\((.*)\)(.*)/) { |m, p|\r
894               @supports[key][:modes] = m.scan(/./).map { |x| x.to_sym}\r
895               @supports[key][:prefixes] = p.scan(/./).map { |x| x.to_sym}\r
896             }\r
897           else\r
898             @supports[key][:modes] = nil\r
899             @supports[key][:prefixes] = nil\r
900           end\r
901         when :safelist\r
902           val_warn(key, val) {\r
903             @supports[key] = val.nil? ? true : val\r
904           }\r
905         when :statusmsg\r
906           noval_warn(key, val) {\r
907             @supports[key] = val.scan(/./)\r
908           }\r
909         when :std\r
910           noval_warn(key, val) {\r
911             @supports[key] = val.split(',')\r
912           }\r
913         else\r
914           @supports[key] =  val.nil? ? true : val\r
915         end\r
916       }\r
917       reparse.gsub!("(chantypes)",@supports[:chantypes])\r
918       parse_isupport(reparse) unless reparse.empty?\r
919     end\r
920 \r
921     # Returns the casemap of the server.\r
922     #\r
923     def casemap\r
924       @supports[:casemapping] || 'rfc1459'\r
925     end\r
926 \r
927     # Returns User or Channel depending on what _name_ can be\r
928     # a name of\r
929     #\r
930     def user_or_channel?(name)\r
931       if supports[:chantypes].include?(name[0].chr)\r
932         return Channel\r
933       else\r
934         return User\r
935       end\r
936     end\r
937 \r
938     # Returns the actual User or Channel object matching _name_\r
939     #\r
940     def user_or_channel(name)\r
941       if supports[:chantypes].include?(name[0].chr)\r
942         return channel(name)\r
943       else\r
944         return user(name)\r
945       end\r
946     end\r
947 \r
948     # Checks if the receiver already has a channel with the given _name_\r
949     #\r
950     def has_channel?(name)\r
951       @channel_names.index(name.to_s)\r
952     end\r
953     alias :has_chan? :has_channel?\r
954 \r
955     # Returns the channel with name _name_, if available\r
956     #\r
957     def get_channel(name)\r
958       idx = @channel_names.index(name.to_s)\r
959       @channels[idx] if idx\r
960     end\r
961     alias :get_chan :get_channel\r
962 \r
963     # Create a new Channel object and add it to the list of\r
964     # <code>Channel</code>s on the receiver, unless the channel\r
965     # was present already. In this case, the default action is\r
966     # to raise an exception, unless _fails_ is set to false\r
967     #\r
968     # The Channel is automatically created with the appropriate casemap\r
969     #\r
970     def new_channel(name, topic=nil, users=[], fails=true)\r
971       if !has_chan?(name)\r
972 \r
973         prefix = name[0].chr\r
974 \r
975         # Give a warning if the new Channel goes over some server limits.\r
976         #\r
977         # FIXME might need to raise an exception\r
978         #\r
979         warn "#{self} doesn't support channel prefix #{prefix}" unless @supports[:chantypes].include?(prefix)\r
980         warn "#{self} doesn't support channel names this long (#{name.length} > #{@support[:channellen]}" unless name.length <= @supports[:channellen]\r
981 \r
982         # Next, we check if we hit the limit for channels of type +prefix+\r
983         # if the server supports +chanlimit+\r
984         #\r
985         @supports[:chanlimit].keys.each { |k|\r
986           next unless k.include?(prefix)\r
987           count = 0\r
988           @channel_names.each { |n|\r
989             count += 1 if k.include?(n[0].chr)\r
990           }\r
991           raise IndexError, "Already joined #{count} channels with prefix #{k}" if count == @supports[:chanlimit][k]\r
992         }\r
993 \r
994         # So far, everything is fine. Now create the actual Channel\r
995         #\r
996         chan = Channel.new(self, name, topic, users)\r
997 \r
998         # We wade through +prefix+ and +chanmodes+ to create appropriate\r
999         # lists and flags for this channel\r
1000 \r
1001         @supports[:prefix][:modes].each { |mode|\r
1002           chan.create_mode(mode, ChannelUserMode)\r
1003         } if @supports[:prefix][:modes]\r
1004 \r
1005         @supports[:chanmodes].each { |k, val|\r
1006           if val\r
1007             case k\r
1008             when :typea\r
1009               val.each { |mode|\r
1010                 chan.create_mode(mode, ChannelModeTypeA)\r
1011               }\r
1012             when :typeb\r
1013               val.each { |mode|\r
1014                 chan.create_mode(mode, ChannelModeTypeB)\r
1015               }\r
1016             when :typec\r
1017               val.each { |mode|\r
1018                 chan.create_mode(mode, ChannelModeTypeC)\r
1019               }\r
1020             when :typed\r
1021               val.each { |mode|\r
1022                 chan.create_mode(mode, ChannelModeTypeD)\r
1023               }\r
1024             end\r
1025           end\r
1026         }\r
1027 \r
1028         @channels << chan\r
1029         @channel_names << name\r
1030         debug "Created channel #{chan.inspect}"\r
1031         debug "Managing channels #{@channel_names.join(', ')}"\r
1032         return chan\r
1033       end\r
1034 \r
1035       raise "Channel #{name} already exists on server #{self}" if fails\r
1036       return get_channel(name)\r
1037     end\r
1038 \r
1039     # Returns the Channel with the given _name_ on the server,\r
1040     # creating it if necessary. This is a short form for\r
1041     # new_channel(_str_, nil, [], +false+)\r
1042     #\r
1043     def channel(str)\r
1044       new_channel(str,nil,[],false)\r
1045     end\r
1046 \r
1047     # Remove Channel _name_ from the list of <code>Channel</code>s\r
1048     #\r
1049     def delete_channel(name)\r
1050       idx = has_channel?(name)\r
1051       raise "Tried to remove unmanaged channel #{name}" unless idx\r
1052       @channel_names.delete_at(idx)\r
1053       @channels.delete_at(idx)\r
1054     end\r
1055 \r
1056     # Checks if the receiver already has a user with the given _nick_\r
1057     #\r
1058     def has_user?(nick)\r
1059       @user_nicks.index(nick.to_s)\r
1060     end\r
1061 \r
1062     # Returns the user with nick _nick_, if available\r
1063     #\r
1064     def get_user(nick)\r
1065       idx = @user_nicks.index(nick.to_s)\r
1066       @users[idx] if idx\r
1067     end\r
1068 \r
1069     # Create a new User object and add it to the list of\r
1070     # <code>User</code>s on the receiver, unless the User\r
1071     # was present already. In this case, the default action is\r
1072     # to raise an exception, unless _fails_ is set to false\r
1073     #\r
1074     # The User is automatically created with the appropriate casemap\r
1075     #\r
1076     def new_user(str, fails=true)\r
1077       case str\r
1078       when User\r
1079         tmp = str\r
1080       else\r
1081         tmp = User.new(str, self.casemap)\r
1082       end\r
1083       if !has_user?(tmp.nick)\r
1084         warn "#{self} doesn't support nicknames this long (#{tmp.nick.length} > #{@support[:nicklen]}" unless tmp.nick.length <= @supports[:nicklen]\r
1085         @users << tmp\r
1086         @user_nicks << tmp.nick\r
1087         return @users.last\r
1088       end\r
1089       old = get_user(tmp.nick)\r
1090       if old.known?\r
1091         raise "User #{tmp.nick} has inconsistent Netmasks! #{self} knows #{old} but access was tried with #{tmp}" if old != tmp\r
1092         raise "User #{tmp} already exists on server #{self}" if fails\r
1093       else\r
1094         old.user = tmp.user\r
1095         old.host = tmp.host\r
1096       end\r
1097       return old\r
1098     end\r
1099 \r
1100     # Returns the User with the given Netmask on the server,\r
1101     # creating it if necessary. This is a short form for\r
1102     # new_user(_str_, +false+)\r
1103     #\r
1104     def user(str)\r
1105       new_user(str, false)\r
1106     end\r
1107 \r
1108     # Remove User _someuser_ from the list of <code>User</code>s.\r
1109     # _someuser_ must be specified with the full Netmask.\r
1110     #\r
1111     def delete_user(someuser)\r
1112       idx = has_user?(someuser.nick)\r
1113       raise "Tried to remove unmanaged user #{user}" unless idx\r
1114       have = self.user(someuser)\r
1115       raise "User #{someuser.nick} has inconsistent Netmasks! #{self} knows #{have} but access was tried with #{someuser}" if have != someuser && have.user != "*" && have.host != "*"\r
1116       @channels.each { |ch|\r
1117         delete_user_from_channel(have, ch)\r
1118       }\r
1119       @user_nicks.delete_at(idx)\r
1120       @users.delete_at(idx)\r
1121     end\r
1122 \r
1123     # Create a new Netmask object with the appropriate casemap\r
1124     #\r
1125     def new_netmask(str)\r
1126       if str.class <= Netmask \r
1127         raise "Wrong casemap for Netmask #{str.inspect}" if str.casemap != self.casemap\r
1128         return str\r
1129       end\r
1130       Netmask.new(str, self.casemap)\r
1131     end\r
1132 \r
1133     # Finds all <code>User</code>s on server whose Netmask matches _mask_\r
1134     #\r
1135     def find_users(mask)\r
1136       nm = new_netmask(mask)\r
1137       @users.inject(UserList.new) {\r
1138         |list, user|\r
1139         if user.user == "*" or user.host == "*"\r
1140           list << user if user.nick =~ nm.nick.to_irc_regexp\r
1141         else\r
1142           list << user if user.matches?(nm)\r
1143         end\r
1144         list\r
1145       }\r
1146     end\r
1147 \r
1148     # Deletes User from Channel\r
1149     #\r
1150     def delete_user_from_channel(user, channel)\r
1151       channel.delete_user(user)\r
1152     end\r
1153 \r
1154   end\r
1155 end\r
1156 \r