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