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