]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/irc.rb
Forgot a space
[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 # * Maybe ChannelList and UserList should be HashesOf instead of ArrayOf?\r
8 #   See items marked as TODO Ho\r
9 #++\r
10 # :title: IRC module\r
11 #\r
12 # Basic IRC stuff\r
13 #\r
14 # This module defines the fundamental building blocks for IRC\r
15 #\r
16 # Author:: Giuseppe Bilotta (giuseppe.bilotta@gmail.com)\r
17 # Copyright:: Copyright (c) 2006 Giuseppe Bilotta\r
18 # License:: GPLv2\r
19 \r
20 require 'singleton'\r
21 \r
22 \r
23 # The Irc module is used to keep all IRC-related classes\r
24 # in the same namespace\r
25 #\r
26 module Irc\r
27 \r
28 \r
29   # Due to its Scandinavian origins, IRC has strange case mappings, which\r
30   # consider the characters <tt>{}|^</tt> as the uppercase\r
31   # equivalents of # <tt>[]\~</tt>.\r
32   #\r
33   # This is however not the same on all IRC servers: some use standard ASCII\r
34   # casemapping, other do not consider <tt>^</tt> as the uppercase of\r
35   # <tt>~</tt>\r
36   #\r
37   class Casemap\r
38     @@casemaps = {}\r
39 \r
40     # Create a new casemap with name _name_, uppercase characters _upper_ and\r
41     # lowercase characters _lower_\r
42     #\r
43     def initialize(name, upper, lower)\r
44       @key = name.to_sym\r
45       raise "Casemap #{name.inspect} already exists!" if @@casemaps.has_key?(@key)\r
46       @@casemaps[@key] = {\r
47         :upper => upper,\r
48         :lower => lower,\r
49         :casemap => self\r
50       }\r
51     end\r
52 \r
53     # Returns the Casemap with the given name\r
54     #\r
55     def Casemap.get(name)\r
56       @@casemaps[name.to_sym][:casemap]\r
57     end\r
58 \r
59     # Retrieve the 'uppercase characters' of this Casemap\r
60     #\r
61     def upper\r
62       @@casemaps[@key][:upper]\r
63     end\r
64 \r
65     # Retrieve the 'lowercase characters' of this Casemap\r
66     #\r
67     def lower\r
68       @@casemaps[@key][:lower]\r
69     end\r
70 \r
71     # Return a Casemap based on the receiver\r
72     #\r
73     def to_irc_casemap\r
74       self\r
75     end\r
76 \r
77     # A Casemap is represented by its lower/upper mappings\r
78     #\r
79     def inspect\r
80       "#<#{self.class}:#{'0x%x'% self.object_id}: #{upper.inspect} ~(#{self})~ #{lower.inspect}>"\r
81     end\r
82 \r
83     # As a String we return our name\r
84     #\r
85     def to_s\r
86       @key.to_s\r
87     end\r
88 \r
89     # Two Casemaps are equal if they have the same upper and lower ranges\r
90     #\r
91     def ==(arg)\r
92       other = arg.to_irc_casemap\r
93       return self.upper == other.upper && self.lower == other.lower\r
94     end\r
95 \r
96     # Raise an error if _arg_ and self are not the same Casemap\r
97     #\r
98     def must_be(arg)\r
99       other = arg.to_irc_casemap\r
100       raise "Casemap mismatch (#{self.inspect} != #{other.inspect})" unless self == other\r
101       return true\r
102     end\r
103 \r
104   end\r
105 \r
106   # The rfc1459 casemap\r
107   #\r
108   class RfcCasemap < Casemap\r
109     include Singleton\r
110 \r
111     def initialize\r
112       super('rfc1459', "\x41-\x5e", "\x61-\x7e")\r
113     end\r
114 \r
115   end\r
116   RfcCasemap.instance\r
117 \r
118   # The strict-rfc1459 Casemap\r
119   #\r
120   class StrictRfcCasemap < Casemap\r
121     include Singleton\r
122 \r
123     def initialize\r
124       super('strict-rfc1459', "\x41-\x5d", "\x61-\x7d")\r
125     end\r
126 \r
127   end\r
128   StrictRfcCasemap.instance\r
129 \r
130   # The ascii Casemap\r
131   #\r
132   class AsciiCasemap < Casemap\r
133     include Singleton\r
134 \r
135     def initialize\r
136       super('ascii', "\x41-\x5a", "\x61-\x7a")\r
137     end\r
138 \r
139   end\r
140   AsciiCasemap.instance\r
141 \r
142 \r
143   # This module is included by all classes that are either bound to a server\r
144   # or should have a casemap.\r
145   #\r
146   module ServerOrCasemap\r
147 \r
148     attr_reader :server\r
149 \r
150     # This method initializes the instance variables @server and @casemap\r
151     # according to the values of the hash keys :server and :casemap in _opts_\r
152     #\r
153     def init_server_or_casemap(opts={})\r
154       @server = opts.fetch(:server, nil)\r
155       raise TypeError, "#{@server} is not a valid Irc::Server" if @server and not @server.kind_of?(Server)\r
156 \r
157       @casemap = opts.fetch(:casemap, nil)\r
158       if @server\r
159         if @casemap\r
160           @server.casemap.must_be(@casemap)\r
161           @casemap = nil\r
162         end\r
163       else\r
164         @casemap = (@casemap || 'rfc1459').to_irc_casemap\r
165       end\r
166     end\r
167 \r
168     # This is an auxiliary method: it returns true if the receiver fits the\r
169     # server and casemap specified in _opts_, false otherwise.\r
170     #\r
171     def fits_with_server_and_casemap?(opts={})\r
172       srv = opts.fetch(:server, nil)\r
173       cmap = opts.fetch(:casemap, nil)\r
174       cmap = cmap.to_irc_casemap unless cmap.nil?\r
175 \r
176       if srv.nil?\r
177         return true if cmap.nil? or cmap == casemap\r
178       else\r
179         return true if srv == @server and (cmap.nil? or cmap == casemap)\r
180       end\r
181       return false\r
182     end\r
183 \r
184     # Returns the casemap of the receiver, by looking at the bound\r
185     # @server (if possible) or at the @casemap otherwise\r
186     #\r
187     def casemap\r
188       return @server.casemap if defined?(@server) and @server\r
189       return @casemap\r
190     end\r
191 \r
192     # Returns a hash with the current @server and @casemap as values of\r
193     # :server and :casemap\r
194     #\r
195     def server_and_casemap\r
196       h = {}\r
197       h[:server] = @server if defined?(@server) and @server\r
198       h[:casemap] = @casemap if defined?(@casemap) and @casemap\r
199       return h\r
200     end\r
201 \r
202     # We allow up/downcasing with a different casemap\r
203     #\r
204     def irc_downcase(cmap=casemap)\r
205       self.to_s.irc_downcase(cmap)\r
206     end\r
207 \r
208     # Up/downcasing something that includes this module returns its\r
209     # Up/downcased to_s form\r
210     #\r
211     def downcase\r
212       self.irc_downcase\r
213     end\r
214 \r
215     # We allow up/downcasing with a different casemap\r
216     #\r
217     def irc_upcase(cmap=casemap)\r
218       self.to_s.irc_upcase(cmap)\r
219     end\r
220 \r
221     # Up/downcasing something that includes this module returns its\r
222     # Up/downcased to_s form\r
223     #\r
224     def upcase\r
225       self.irc_upcase\r
226     end\r
227 \r
228   end\r
229 \r
230 end\r
231 \r
232 \r
233 # We start by extending the String class\r
234 # with some IRC-specific methods\r
235 #\r
236 class String\r
237 \r
238   # This method returns the Irc::Casemap whose name is the receiver\r
239   #\r
240   def to_irc_casemap\r
241     Irc::Casemap.get(self) rescue raise TypeError, "Unkown Irc::Casemap #{self.inspect}"\r
242   end\r
243 \r
244   # This method returns a string which is the downcased version of the\r
245   # receiver, according to the given _casemap_\r
246   #\r
247   #\r
248   def irc_downcase(casemap='rfc1459')\r
249     cmap = casemap.to_irc_casemap\r
250     self.tr(cmap.upper, cmap.lower)\r
251   end\r
252 \r
253   # This is the same as the above, except that the string is altered in place\r
254   #\r
255   # See also the discussion about irc_downcase\r
256   #\r
257   def irc_downcase!(casemap='rfc1459')\r
258     cmap = casemap.to_irc_casemap\r
259     self.tr!(cmap.upper, cmap.lower)\r
260   end\r
261 \r
262   # Upcasing functions are provided too\r
263   #\r
264   # See also the discussion about irc_downcase\r
265   #\r
266   def irc_upcase(casemap='rfc1459')\r
267     cmap = casemap.to_irc_casemap\r
268     self.tr(cmap.lower, cmap.upper)\r
269   end\r
270 \r
271   # In-place upcasing\r
272   #\r
273   # See also the discussion about irc_downcase\r
274   #\r
275   def irc_upcase!(casemap='rfc1459')\r
276     cmap = casemap.to_irc_casemap\r
277     self.tr!(cmap.lower, cmap.upper)\r
278   end\r
279 \r
280   # This method checks if the receiver contains IRC glob characters\r
281   #\r
282   # IRC has a very primitive concept of globs: a <tt>*</tt> stands for "any\r
283   # number of arbitrary characters", a <tt>?</tt> stands for "one and exactly\r
284   # one arbitrary character". These characters can be escaped by prefixing them\r
285   # with a slash (<tt>\\</tt>).\r
286   #\r
287   # A known limitation of this glob syntax is that there is no way to escape\r
288   # the escape character itself, so it's not possible to build a glob pattern\r
289   # where the escape character precedes a glob.\r
290   #\r
291   def has_irc_glob?\r
292     self =~ /^[*?]|[^\\][*?]/\r
293   end\r
294 \r
295   # This method is used to convert the receiver into a Regular Expression\r
296   # that matches according to the IRC glob syntax\r
297   #\r
298   def to_irc_regexp\r
299     regmask = Regexp.escape(self)\r
300     regmask.gsub!(/(\\\\)?\\[*?]/) { |m|\r
301       case m\r
302       when /\\(\\[*?])/\r
303         $1\r
304       when /\\\*/\r
305         '.*'\r
306       when /\\\?/\r
307         '.'\r
308       else\r
309         raise "Unexpected match #{m} when converting #{self}"\r
310       end\r
311     }\r
312     Regexp.new(regmask)\r
313   end\r
314 \r
315 end\r
316 \r
317 \r
318 # ArrayOf is a subclass of Array whose elements are supposed to be all\r
319 # of the same class. This is not intended to be used directly, but rather\r
320 # to be subclassed as needed (see for example Irc::UserList and Irc::NetmaskList)\r
321 #\r
322 # Presently, only very few selected methods from Array are overloaded to check\r
323 # if the new elements are the correct class. An orthodox? method is provided\r
324 # to check the entire ArrayOf against the appropriate class.\r
325 #\r
326 class ArrayOf < Array\r
327 \r
328   attr_reader :element_class\r
329 \r
330   # Create a new ArrayOf whose elements are supposed to be all of type _kl_,\r
331   # optionally filling it with the elements from the Array argument.\r
332   #\r
333   def initialize(kl, ar=[])\r
334     raise TypeError, "#{kl.inspect} must be a class name" unless kl.kind_of?(Class)\r
335     super()\r
336     @element_class = kl\r
337     case ar\r
338     when Array\r
339       insert(0, *ar)\r
340     else\r
341       raise TypeError, "#{self.class} can only be initialized from an Array"\r
342     end\r
343   end\r
344 \r
345   def inspect\r
346     "#<#{self.class}[#{@element_class}]:#{'0x%x' % self.object_id}: #{super}>"\r
347   end\r
348 \r
349   # Private method to check the validity of the elements passed to it\r
350   # and optionally raise an error\r
351   #\r
352   # TODO should it accept nils as valid?\r
353   #\r
354   def internal_will_accept?(raising, *els)\r
355     els.each { |el|\r
356       unless el.kind_of?(@element_class)\r
357         raise TypeError, "#{el.inspect} is not of class #{@element_class}" if raising\r
358         return false\r
359       end\r
360     }\r
361     return true\r
362   end\r
363   private :internal_will_accept?\r
364 \r
365   # This method checks if the passed arguments are acceptable for our ArrayOf\r
366   #\r
367   def will_accept?(*els)\r
368     internal_will_accept?(false, *els)\r
369   end\r
370 \r
371   # This method checks that all elements are of the appropriate class\r
372   #\r
373   def valid?\r
374     will_accept?(*self)\r
375   end\r
376 \r
377   # This method is similar to the above, except that it raises an exception\r
378   # if the receiver is not valid\r
379   #\r
380   def validate\r
381     raise TypeError unless valid?\r
382   end\r
383 \r
384   # Overloaded from Array#<<, checks for appropriate class of argument\r
385   #\r
386   def <<(el)\r
387     super(el) if internal_will_accept?(true, el)\r
388   end\r
389 \r
390   # Overloaded from Array#&, checks for appropriate class of argument elements\r
391   #\r
392   def &(ar)\r
393     r = super(ar)\r
394     ArrayOf.new(@element_class, r) if internal_will_accept?(true, *r)\r
395   end\r
396 \r
397   # Overloaded from Array#+, checks for appropriate class of argument elements\r
398   #\r
399   def +(ar)\r
400     ArrayOf.new(@element_class, super(ar)) if internal_will_accept?(true, *ar)\r
401   end\r
402 \r
403   # Overloaded from Array#-, so that an ArrayOf is returned. There is no need\r
404   # to check the validity of the elements in the argument\r
405   #\r
406   def -(ar)\r
407     ArrayOf.new(@element_class, super(ar)) # if internal_will_accept?(true, *ar)\r
408   end\r
409 \r
410   # Overloaded from Array#|, checks for appropriate class of argument elements\r
411   #\r
412   def |(ar)\r
413     ArrayOf.new(@element_class, super(ar)) if internal_will_accept?(true, *ar)\r
414   end\r
415 \r
416   # Overloaded from Array#concat, checks for appropriate class of argument\r
417   # elements\r
418   #\r
419   def concat(ar)\r
420     super(ar) if internal_will_accept?(true, *ar)\r
421   end\r
422 \r
423   # Overloaded from Array#insert, checks for appropriate class of argument\r
424   # elements\r
425   #\r
426   def insert(idx, *ar)\r
427     super(idx, *ar) if internal_will_accept?(true, *ar)\r
428   end\r
429 \r
430   # Overloaded from Array#replace, checks for appropriate class of argument\r
431   # elements\r
432   #\r
433   def replace(ar)\r
434     super(ar) if (ar.kind_of?(ArrayOf) && ar.element_class <= @element_class) or internal_will_accept?(true, *ar)\r
435   end\r
436 \r
437   # Overloaded from Array#push, checks for appropriate class of argument\r
438   # elements\r
439   #\r
440   def push(*ar)\r
441     super(*ar) if internal_will_accept?(true, *ar)\r
442   end\r
443 \r
444   # Overloaded from Array#unshift, checks for appropriate class of argument(s)\r
445   #\r
446   def unshift(*els)\r
447     els.each { |el|\r
448       super(el) if internal_will_accept?(true, *els)\r
449     }\r
450   end\r
451 \r
452   # Modifying methods which we don't handle yet are made private\r
453   #\r
454   private :[]=, :collect!, :map!, :fill, :flatten!\r
455 \r
456 end\r
457 \r
458 \r
459 module Irc\r
460 \r
461 \r
462   # A Netmask identifies each user by collecting its nick, username and\r
463   # hostname in the form <tt>nick!user@host</tt>\r
464   #\r
465   # Netmasks can also contain glob patterns in any of their components; in\r
466   # this form they are used to refer to more than a user or to a user\r
467   # appearing under different forms.\r
468   #\r
469   # Example:\r
470   # * <tt>*!*@*</tt> refers to everybody\r
471   # * <tt>*!someuser@somehost</tt> refers to user +someuser+ on host +somehost+\r
472   #   regardless of the nick used.\r
473   #\r
474   class Netmask\r
475 \r
476     # Netmasks have an associated casemap unless they are bound to a server\r
477     #\r
478     include ServerOrCasemap\r
479 \r
480     attr_reader :nick, :user, :host\r
481 \r
482     # Create a new Netmask from string _str_, which must be in the form\r
483     # _nick_!_user_@_host_\r
484     #\r
485     # It is possible to specify a server or a casemap in the optional Hash:\r
486     # these are used to associate the Netmask with the given server and to set\r
487     # its casemap: if a server is specified and a casemap is not, the server's\r
488     # casemap is used. If both a server and a casemap are specified, the\r
489     # casemap must match the server's casemap or an exception will be raised.\r
490     #\r
491     # Empty +nick+, +user+ or +host+ are converted to the generic glob pattern\r
492     #\r
493     def initialize(str="", opts={})\r
494       # First of all, check for server/casemap option\r
495       #\r
496       init_server_or_casemap(opts)\r
497 \r
498       # Now we can see if the given string _str_ is an actual Netmask\r
499       if str.respond_to?(:to_str)\r
500         case str.to_str\r
501         when /^(?:(\S+?)(?:!(\S+)@(?:(\S+))?)?)?$/\r
502           # We do assignment using our internal methods\r
503           self.nick = $1\r
504           self.user = $2\r
505           self.host = $3\r
506         else\r
507           raise ArgumentError, "#{str.to_str.inspect} does not represent a valid #{self.class}"\r
508         end\r
509       else\r
510         raise TypeError, "#{str} cannot be converted to a #{self.class}"\r
511       end\r
512     end\r
513 \r
514     # A Netmask is easily converted to a String for the usual representation\r
515     #\r
516     def fullform\r
517       "#{nick}!#{user}@#{host}"\r
518     end\r
519     alias :to_s :fullform\r
520 \r
521     # Converts the receiver into a Netmask with the given (optional)\r
522     # server/casemap association. We return self unless a conversion\r
523     # is needed (different casemap/server)\r
524     #\r
525     # Subclasses of Netmask will return a new Netmask\r
526     #\r
527     def to_irc_netmask(opts={})\r
528       if self.class == Netmask\r
529         return self if fits_with_server_and_casemap?(opts)\r
530       end\r
531       return self.fullform.to_irc_netmask(server_and_casemap.merge(opts))\r
532     end\r
533 \r
534     # Converts the receiver into a User with the given (optional)\r
535     # server/casemap association. We return self unless a conversion\r
536     # is needed (different casemap/server)\r
537     #\r
538     def to_irc_user(opts={})\r
539       self.fullform.to_irc_user(server_and_casemap.merge(opts))\r
540     end\r
541 \r
542     # Inspection of a Netmask reveals the server it's bound to (if there is\r
543     # one), its casemap and the nick, user and host part\r
544     #\r
545     def inspect\r
546       str = "<#{self.class}:#{'0x%x' % self.object_id}:"\r
547       str << " @server=#{@server}" if defined?(@server) and @server\r
548       str << " @nick=#{@nick.inspect} @user=#{@user.inspect}"\r
549       str << " @host=#{@host.inspect} casemap=#{casemap.inspect}"\r
550       str << ">"\r
551     end\r
552 \r
553     # Equality: two Netmasks are equal if they downcase to the same thing\r
554     #\r
555     # TODO we may want it to try other.to_irc_netmask\r
556     #\r
557     def ==(other)\r
558       return false unless other.kind_of?(self.class)\r
559       self.downcase == other.downcase\r
560     end\r
561 \r
562     # This method changes the nick of the Netmask, defaulting to the generic\r
563     # glob pattern if the result is the null string.\r
564     #\r
565     def nick=(newnick)\r
566       @nick = newnick.to_s\r
567       @nick = "*" if @nick.empty?\r
568     end\r
569 \r
570     # This method changes the user of the Netmask, defaulting to the generic\r
571     # glob pattern if the result is the null string.\r
572     #\r
573     def user=(newuser)\r
574       @user = newuser.to_s\r
575       @user = "*" if @user.empty?\r
576     end\r
577 \r
578     # This method changes the hostname of the Netmask, defaulting to the generic\r
579     # glob pattern if the result is the null string.\r
580     #\r
581     def host=(newhost)\r
582       @host = newhost.to_s\r
583       @host = "*" if @host.empty?\r
584     end\r
585 \r
586     # We can replace everything at once with data from another Netmask\r
587     #\r
588     def replace(other)\r
589       case other\r
590       when Netmask\r
591         nick = other.nick\r
592         user = other.user\r
593         host = other.host\r
594         @server = other.server\r
595         @casemap = other.casemap unless @server\r
596       else\r
597         replace(other.to_irc_netmask(server_and_casemap))\r
598       end\r
599     end\r
600 \r
601     # This method checks if a Netmask is definite or not, by seeing if\r
602     # any of its components are defined by globs\r
603     #\r
604     def has_irc_glob?\r
605       return @nick.has_irc_glob? || @user.has_irc_glob? || @host.has_irc_glob?\r
606     end\r
607 \r
608     # This method is used to match the current Netmask against another one\r
609     #\r
610     # The method returns true if each component of the receiver matches the\r
611     # corresponding component of the argument. By _matching_ here we mean\r
612     # that any netmask described by the receiver is also described by the\r
613     # argument.\r
614     #\r
615     # In this sense, matching is rather simple to define in the case when the\r
616     # receiver has no globs: it is just necessary to check if the argument\r
617     # describes the receiver, which can be done by matching it against the\r
618     # argument converted into an IRC Regexp (see String#to_irc_regexp).\r
619     #\r
620     # The situation is also easy when the receiver has globs and the argument\r
621     # doesn't, since in this case the result is false.\r
622     #\r
623     # The more complex case in which both the receiver and the argument have\r
624     # globs is not handled yet.\r
625     #\r
626     def matches?(arg)\r
627       cmp = arg.to_irc_netmask(:casemap => casemap)\r
628       debug "Matching #{self.fullform} against #{arg.fullform}"\r
629       [:nick, :user, :host].each { |component|\r
630         us = self.send(component).irc_downcase(casemap)\r
631         them = cmp.send(component).irc_downcase(casemap)\r
632         if us.has_irc_glob? && them.has_irc_glob?\r
633           next if us == them\r
634           warn NotImplementedError\r
635           return false\r
636         end\r
637         return false if us.has_irc_glob? && !them.has_irc_glob?\r
638         return false unless us =~ them.to_irc_regexp\r
639       }\r
640       return true\r
641     end\r
642 \r
643     # Case equality. Checks if arg matches self\r
644     #\r
645     def ===(arg)\r
646       arg.to_irc_netmask(:casemap => casemap).matches?(self)\r
647     end\r
648 \r
649     # Sorting is done via the fullform\r
650     #\r
651     def <=>(arg)\r
652       case arg\r
653       when Netmask\r
654         self.fullform.irc_downcase(casemap) <=> arg.fullform.irc_downcase(casemap)\r
655       else\r
656         self.downcase <=> arg.downcase\r
657       end\r
658     end\r
659 \r
660   end\r
661 \r
662 \r
663   # A NetmaskList is an ArrayOf <code>Netmask</code>s\r
664   #\r
665   class NetmaskList < ArrayOf\r
666 \r
667     # Create a new NetmaskList, optionally filling it with the elements from\r
668     # the Array argument fed to it.\r
669     #\r
670     def initialize(ar=[])\r
671       super(Netmask, ar)\r
672     end\r
673 \r
674   end\r
675 \r
676 end\r
677 \r
678 \r
679 class String\r
680 \r
681   # We keep extending String, this time adding a method that converts a\r
682   # String into an Irc::Netmask object\r
683   #\r
684   def to_irc_netmask(opts={})\r
685     Irc::Netmask.new(self, opts)\r
686   end\r
687 \r
688 end\r
689 \r
690 \r
691 module Irc\r
692 \r
693 \r
694   # An IRC User is identified by his/her Netmask (which must not have globs).\r
695   # In fact, User is just a subclass of Netmask.\r
696   #\r
697   # Ideally, the user and host information of an IRC User should never\r
698   # change, and it shouldn't contain glob patterns. However, IRC is somewhat\r
699   # idiosincratic and it may be possible to know the nick of a User much before\r
700   # its user and host are known. Moreover, some networks (namely Freenode) may\r
701   # change the hostname of a User when (s)he identifies with Nickserv.\r
702   #\r
703   # As a consequence, we must allow changes to a User host and user attributes.\r
704   # We impose a restriction, though: they may not contain glob patterns, except\r
705   # for the special case of an unknown user/host which is represented by a *.\r
706   #\r
707   # It is possible to create a totally unknown User (e.g. for initializations)\r
708   # by setting the nick to * too.\r
709   #\r
710   # TODO list:\r
711   # * see if it's worth to add the other USER data\r
712   # * see if it's worth to add NICKSERV status\r
713   #\r
714   class User < Netmask\r
715     alias :to_s :nick\r
716 \r
717     # Create a new IRC User from a given Netmask (or anything that can be converted\r
718     # into a Netmask) provided that the given Netmask does not have globs.\r
719     #\r
720     def initialize(str="", opts={})\r
721       super\r
722       raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if nick.has_irc_glob? && nick != "*"\r
723       raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if user.has_irc_glob? && user != "*"\r
724       raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if host.has_irc_glob? && host != "*"\r
725       @away = false\r
726     end\r
727 \r
728     # The nick of a User may be changed freely, but it must not contain glob patterns.\r
729     #\r
730     def nick=(newnick)\r
731       raise "Can't change the nick to #{newnick}" if defined?(@nick) and newnick.has_irc_glob?\r
732       super\r
733     end\r
734 \r
735     # We have to allow changing the user of an Irc User due to some networks\r
736     # (e.g. Freenode) changing hostmasks on the fly. We still check if the new\r
737     # user data has glob patterns though.\r
738     #\r
739     def user=(newuser)\r
740       raise "Can't change the username to #{newuser}" if defined?(@user) and newuser.has_irc_glob?\r
741       super\r
742     end\r
743 \r
744     # We have to allow changing the host of an Irc User due to some networks\r
745     # (e.g. Freenode) changing hostmasks on the fly. We still check if the new\r
746     # host data has glob patterns though.\r
747     #\r
748     def host=(newhost)\r
749       raise "Can't change the hostname to #{newhost}" if defined?(@host) and newhost.has_irc_glob?\r
750       super\r
751     end\r
752 \r
753     # Checks if a User is well-known or not by looking at the hostname and user\r
754     #\r
755     def known?\r
756       return nick!= "*" && user!="*" && host!="*"\r
757     end\r
758 \r
759     # Is the user away?\r
760     #\r
761     def away?\r
762       return @away\r
763     end\r
764 \r
765     # Set the away status of the user. Use away=(nil) or away=(false)\r
766     # to unset away\r
767     #\r
768     def away=(msg="")\r
769       if msg\r
770         @away = msg\r
771       else\r
772         @away = false\r
773       end\r
774     end\r
775 \r
776     # Since to_irc_user runs the same checks on server and channel as\r
777     # to_irc_netmask, we just try that and return self if it works.\r
778     #\r
779     # Subclasses of User will return self if possible.\r
780     #\r
781     def to_irc_user(opts={})\r
782       return self if fits_with_server_and_casemap?(opts)\r
783       return self.fullform.to_irc_user(server_and_casemap(opts))\r
784     end\r
785 \r
786     # We can replace everything at once with data from another User\r
787     #\r
788     def replace(other)\r
789       case other\r
790       when User\r
791         self.nick = other.nick\r
792         self.user = other.user\r
793         self.host = other.host\r
794         @server = other.server\r
795         @casemap = other.casemap unless @server\r
796         @away = other.away?\r
797       else\r
798         self.replace(other.to_irc_user(server_and_casemap))\r
799       end\r
800     end\r
801 \r
802   end\r
803 \r
804 \r
805   # A UserList is an ArrayOf <code>User</code>s\r
806   #\r
807   class UserList < ArrayOf\r
808 \r
809     # Create a new UserList, optionally filling it with the elements from\r
810     # the Array argument fed to it.\r
811     #\r
812     def initialize(ar=[])\r
813       super(User, ar)\r
814     end\r
815 \r
816   end\r
817 \r
818 end\r
819 \r
820 class String\r
821 \r
822   # We keep extending String, this time adding a method that converts a\r
823   # String into an Irc::User object\r
824   #\r
825   def to_irc_user(opts={})\r
826     Irc::User.new(self, opts)\r
827   end\r
828 \r
829 end\r
830 \r
831 module Irc\r
832 \r
833   # An IRC Channel is identified by its name, and it has a set of properties:\r
834   # * a Channel::Topic\r
835   # * a UserList\r
836   # * a set of Channel::Modes\r
837   #\r
838   # The Channel::Topic and Channel::Mode classes are defined within the\r
839   # Channel namespace because they only make sense there\r
840   #\r
841   class Channel\r
842 \r
843 \r
844     # Mode on a Channel\r
845     #\r
846     class Mode\r
847       def initialize(ch)\r
848         @channel = ch\r
849       end\r
850 \r
851     end\r
852 \r
853 \r
854     # Channel modes of type A manipulate lists\r
855     #\r
856     # Example: b (banlist)\r
857     #\r
858     class ModeTypeA < Mode\r
859       def initialize(ch)\r
860         super\r
861         @list = NetmaskList.new\r
862       end\r
863 \r
864       def set(val)\r
865         nm = @channel.server.new_netmask(val)\r
866         @list << nm unless @list.include?(nm)\r
867       end\r
868 \r
869       def reset(val)\r
870         nm = @channel.server.new_netmask(val)\r
871         @list.delete(nm)\r
872       end\r
873 \r
874     end\r
875 \r
876 \r
877     # Channel modes of type B need an argument\r
878     #\r
879     # Example: k (key)\r
880     #\r
881     class ModeTypeB < Mode\r
882       def initialize(ch)\r
883         super\r
884         @arg = nil\r
885       end\r
886 \r
887       def set(val)\r
888         @arg = val\r
889       end\r
890 \r
891       def reset(val)\r
892         @arg = nil if @arg == val\r
893       end\r
894 \r
895     end\r
896 \r
897 \r
898     # Channel modes that change the User prefixes are like\r
899     # Channel modes of type B, except that they manipulate\r
900     # lists of Users, so they are somewhat similar to channel\r
901     # modes of type A\r
902     #\r
903     class UserMode < ModeTypeB\r
904       def initialize(ch)\r
905         super\r
906         @list = UserList.new\r
907       end\r
908 \r
909       def set(val)\r
910         u = @channel.server.user(val)\r
911         @list << u unless @list.include?(u)\r
912       end\r
913 \r
914       def reset(val)\r
915         u = @channel.server.user(val)\r
916         @list.delete(u)\r
917       end\r
918 \r
919     end\r
920 \r
921 \r
922     # Channel modes of type C need an argument when set,\r
923     # but not when they get reset\r
924     #\r
925     # Example: l (limit)\r
926     #\r
927     class ModeTypeC < Mode\r
928       def initialize(ch)\r
929         super\r
930         @arg = false\r
931       end\r
932 \r
933       def status\r
934         @arg\r
935       end\r
936 \r
937       def set(val)\r
938         @arg = val\r
939       end\r
940 \r
941       def reset\r
942         @arg = false\r
943       end\r
944 \r
945     end\r
946 \r
947 \r
948     # Channel modes of type D are basically booleans\r
949     #\r
950     # Example: m (moderate)\r
951     #\r
952     class ModeTypeD < Mode\r
953       def initialize(ch)\r
954         super\r
955         @set = false\r
956       end\r
957 \r
958       def set?\r
959         return @set\r
960       end\r
961 \r
962       def set\r
963         @set = true\r
964       end\r
965 \r
966       def reset\r
967         @set = false\r
968       end\r
969 \r
970     end\r
971 \r
972 \r
973     # A Topic represents the topic of a channel. It consists of\r
974     # the topic itself, who set it and when\r
975     #\r
976     class Topic\r
977       attr_accessor :text, :set_by, :set_on\r
978       alias :to_s :text\r
979 \r
980       # Create a new Topic setting the text, the creator and\r
981       # the creation time\r
982       #\r
983       def initialize(text="", set_by="", set_on=Time.new)\r
984         @text = text\r
985         @set_by = set_by.to_irc_user\r
986         @set_on = set_on\r
987       end\r
988 \r
989       # Replace a Topic with another one\r
990       #\r
991       def replace(topic)\r
992         raise TypeError, "#{topic.inspect} is not of class #{self.class}" unless topic.kind_of?(self.class)\r
993         @text = topic.text.dup\r
994         @set_by = topic.set_by.dup\r
995         @set_on = topic.set_on.dup\r
996       end\r
997 \r
998       # Returns self\r
999       #\r
1000       def to_irc_channel_topic\r
1001         self\r
1002       end\r
1003 \r
1004     end\r
1005 \r
1006   end\r
1007 \r
1008 end\r
1009 \r
1010 \r
1011 class String\r
1012 \r
1013   # Returns an Irc::Channel::Topic with self as text\r
1014   #\r
1015   def to_irc_channel_topic\r
1016     Irc::Channel::Topic.new(self)\r
1017   end\r
1018 \r
1019 end\r
1020 \r
1021 \r
1022 module Irc\r
1023 \r
1024 \r
1025   # Here we start with the actual Channel class\r
1026   #\r
1027   class Channel\r
1028 \r
1029     include ServerOrCasemap\r
1030     attr_reader :name, :topic, :mode, :users\r
1031     alias :to_s :name\r
1032 \r
1033     def inspect\r
1034       str = "<#{self.class}:#{'0x%x' % self.object_id}:"\r
1035       str << " on server #{server}" if server\r
1036       str << " @name=#{@name.inspect} @topic=#{@topic.text.inspect}"\r
1037       str << " @users=[#{user_nicks.sort.join(', ')}]"\r
1038       str << ">"\r
1039     end\r
1040 \r
1041     # Returns self\r
1042     #\r
1043     def to_irc_channel\r
1044       self\r
1045     end\r
1046 \r
1047     # TODO Ho\r
1048     def user_nicks\r
1049       @users.map { |u| u.downcase }\r
1050     end\r
1051 \r
1052     # Checks if the receiver already has a user with the given _nick_\r
1053     #\r
1054     def has_user?(nick)\r
1055       user_nicks.index(nick.irc_downcase(casemap))\r
1056     end\r
1057 \r
1058     # Returns the user with nick _nick_, if available\r
1059     #\r
1060     def get_user(nick)\r
1061       idx = has_user?(nick)\r
1062       @users[idx] if idx\r
1063     end\r
1064 \r
1065     # Adds a user to the channel\r
1066     #\r
1067     def add_user(user, opts={})\r
1068       silent = opts.fetch(:silent, false) \r
1069       if has_user?(user) && !silent\r
1070         warn "Trying to add user #{user} to channel #{self} again"\r
1071       else\r
1072         @users << user.to_irc_user(server_and_casemap)\r
1073       end\r
1074     end\r
1075 \r
1076     # Creates a new channel with the given name, optionally setting the topic\r
1077     # and an initial users list.\r
1078     #\r
1079     # No additional info is created here, because the channel flags and userlists\r
1080     # allowed depend on the server.\r
1081     #\r
1082     def initialize(name, topic=nil, users=[], opts={})\r
1083       raise ArgumentError, "Channel name cannot be empty" if name.to_s.empty?\r
1084       warn "Unknown channel prefix #{name[0].chr}" if name !~ /^[&#+!]/\r
1085       raise ArgumentError, "Invalid character in #{name.inspect}" if name =~ /[ \x07,]/\r
1086 \r
1087       init_server_or_casemap(opts)\r
1088 \r
1089       @name = name\r
1090 \r
1091       @topic = (topic.to_irc_channel_topic rescue Channel::Topic.new)\r
1092 \r
1093       @users = UserList.new\r
1094 \r
1095       users.each { |u|\r
1096         add_user(u)\r
1097       }\r
1098 \r
1099       # Flags\r
1100       @mode = {}\r
1101     end\r
1102 \r
1103     # Removes a user from the channel\r
1104     #\r
1105     def delete_user(user)\r
1106       @mode.each { |sym, mode|\r
1107         mode.reset(user) if mode.kind_of?(UserMode)\r
1108       }\r
1109       @users.delete(user)\r
1110     end\r
1111 \r
1112     # The channel prefix\r
1113     #\r
1114     def prefix\r
1115       name[0].chr\r
1116     end\r
1117 \r
1118     # A channel is local to a server if it has the '&' prefix\r
1119     #\r
1120     def local?\r
1121       name[0] == 0x26\r
1122     end\r
1123 \r
1124     # A channel is modeless if it has the '+' prefix\r
1125     #\r
1126     def modeless?\r
1127       name[0] == 0x2b\r
1128     end\r
1129 \r
1130     # A channel is safe if it has the '!' prefix\r
1131     #\r
1132     def safe?\r
1133       name[0] == 0x21\r
1134     end\r
1135 \r
1136     # A channel is normal if it has the '#' prefix\r
1137     #\r
1138     def normal?\r
1139       name[0] == 0x23\r
1140     end\r
1141 \r
1142     # Create a new mode\r
1143     #\r
1144     def create_mode(sym, kl)\r
1145       @mode[sym.to_sym] = kl.new(self)\r
1146     end\r
1147 \r
1148   end\r
1149 \r
1150 \r
1151   # A ChannelList is an ArrayOf <code>Channel</code>s\r
1152   #\r
1153   class ChannelList < ArrayOf\r
1154 \r
1155     # Create a new ChannelList, optionally filling it with the elements from\r
1156     # the Array argument fed to it.\r
1157     #\r
1158     def initialize(ar=[])\r
1159       super(Channel, ar)\r
1160     end\r
1161 \r
1162   end\r
1163 \r
1164 end\r
1165 \r
1166 \r
1167 class String\r
1168 \r
1169   # We keep extending String, this time adding a method that converts a\r
1170   # String into an Irc::Channel object\r
1171   #\r
1172   def to_irc_channel(opts={})\r
1173     Irc::Channel.new(self, opts)\r
1174   end\r
1175 \r
1176 end\r
1177 \r
1178 \r
1179 module Irc\r
1180 \r
1181 \r
1182   # An IRC Server represents the Server the client is connected to.\r
1183   #\r
1184   class Server\r
1185 \r
1186     attr_reader :hostname, :version, :usermodes, :chanmodes\r
1187     alias :to_s :hostname\r
1188     attr_reader :supports, :capabilities\r
1189 \r
1190     attr_reader :channels, :users\r
1191 \r
1192     # TODO Ho\r
1193     def channel_names\r
1194       @channels.map { |ch| ch.downcase }\r
1195     end\r
1196 \r
1197     # TODO Ho\r
1198     def user_nicks\r
1199       @users.map { |u| u.downcase }\r
1200     end\r
1201 \r
1202     def inspect\r
1203       chans, users = [@channels, @users].map {|d|\r
1204         d.sort { |a, b|\r
1205           a.downcase <=> b.downcase\r
1206         }.map { |x|\r
1207           x.inspect\r
1208         }\r
1209       }\r
1210 \r
1211       str = "<#{self.class}:#{'0x%x' % self.object_id}:"\r
1212       str << " @hostname=#{hostname}"\r
1213       str << " @channels=#{chans}"\r
1214       str << " @users=#{users}"\r
1215       str << ">"\r
1216     end\r
1217 \r
1218     # Create a new Server, with all instance variables reset to nil (for\r
1219     # scalar variables), empty channel and user lists and @supports\r
1220     # initialized to the default values for all known supported features.\r
1221     #\r
1222     def initialize\r
1223       @hostname = @version = @usermodes = @chanmodes = nil\r
1224 \r
1225       @channels = ChannelList.new\r
1226 \r
1227       @users = UserList.new\r
1228 \r
1229       reset_capabilities\r
1230     end\r
1231 \r
1232     # Resets the server capabilities\r
1233     #\r
1234     def reset_capabilities\r
1235       @supports = {\r
1236         :casemapping => 'rfc1459'.to_irc_casemap,\r
1237         :chanlimit => {},\r
1238         :chanmodes => {\r
1239           :typea => nil, # Type A: address lists\r
1240           :typeb => nil, # Type B: needs a parameter\r
1241           :typec => nil, # Type C: needs a parameter when set\r
1242           :typed => nil  # Type D: must not have a parameter\r
1243         },\r
1244         :channellen => 200,\r
1245         :chantypes => "#&",\r
1246         :excepts => nil,\r
1247         :idchan => {},\r
1248         :invex => nil,\r
1249         :kicklen => nil,\r
1250         :maxlist => {},\r
1251         :modes => 3,\r
1252         :network => nil,\r
1253         :nicklen => 9,\r
1254         :prefix => {\r
1255           :modes => 'ov'.scan(/./),\r
1256           :prefixes => '@+'.scan(/./)\r
1257         },\r
1258         :safelist => nil,\r
1259         :statusmsg => nil,\r
1260         :std => nil,\r
1261         :targmax => {},\r
1262         :topiclen => nil\r
1263       }\r
1264       @capabilities = {}\r
1265     end\r
1266 \r
1267     # Resets the Channel and User list\r
1268     #\r
1269     def reset_lists\r
1270       @users.each { |u|\r
1271         delete_user(u)\r
1272       }\r
1273       @channels.each { |u|\r
1274         delete_channel(u)\r
1275       }\r
1276     end\r
1277 \r
1278     # Clears the server\r
1279     #\r
1280     def clear\r
1281       reset_lists\r
1282       reset_capabilities\r
1283     end\r
1284 \r
1285     # This method is used to parse a 004 RPL_MY_INFO line\r
1286     #\r
1287     def parse_my_info(line)\r
1288       ar = line.split(' ')\r
1289       @hostname = ar[0]\r
1290       @version = ar[1]\r
1291       @usermodes = ar[2]\r
1292       @chanmodes = ar[3]\r
1293     end\r
1294 \r
1295     def noval_warn(key, val, &block)\r
1296       if val\r
1297         yield if block_given?\r
1298       else\r
1299         warn "No #{key.to_s.upcase} value"\r
1300       end\r
1301     end\r
1302 \r
1303     def val_warn(key, val, &block)\r
1304       if val == true or val == false or val.nil?\r
1305         yield if block_given?\r
1306       else\r
1307         warn "No #{key.to_s.upcase} value must be specified, got #{val}"\r
1308       end\r
1309     end\r
1310     private :noval_warn, :val_warn\r
1311 \r
1312     # This method is used to parse a 005 RPL_ISUPPORT line\r
1313     #\r
1314     # See the RPL_ISUPPORT draft[http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt]\r
1315     #\r
1316     def parse_isupport(line)\r
1317       debug "Parsing ISUPPORT #{line.inspect}"\r
1318       ar = line.split(' ')\r
1319       reparse = ""\r
1320       ar.each { |en|\r
1321         prekey, val = en.split('=', 2)\r
1322         if prekey =~ /^-(.*)/\r
1323           key = $1.downcase.to_sym\r
1324           val = false\r
1325         else\r
1326           key = prekey.downcase.to_sym\r
1327         end\r
1328         case key\r
1329         when :casemapping\r
1330           noval_warn(key, val) {\r
1331             @supports[key] = val.to_irc_casemap\r
1332           }\r
1333         when :chanlimit, :idchan, :maxlist, :targmax\r
1334           noval_warn(key, val) {\r
1335             groups = val.split(',')\r
1336             groups.each { |g|\r
1337               k, v = g.split(':')\r
1338               @supports[key][k] = v.to_i || 0\r
1339             }\r
1340           }\r
1341         when :chanmodes\r
1342           noval_warn(key, val) {\r
1343             groups = val.split(',')\r
1344             @supports[key][:typea] = groups[0].scan(/./).map { |x| x.to_sym}\r
1345             @supports[key][:typeb] = groups[1].scan(/./).map { |x| x.to_sym}\r
1346             @supports[key][:typec] = groups[2].scan(/./).map { |x| x.to_sym}\r
1347             @supports[key][:typed] = groups[3].scan(/./).map { |x| x.to_sym}\r
1348           }\r
1349         when :channellen, :kicklen, :modes, :topiclen\r
1350           if val\r
1351             @supports[key] = val.to_i\r
1352           else\r
1353             @supports[key] = nil\r
1354           end\r
1355         when :chantypes\r
1356           @supports[key] = val # can also be nil\r
1357         when :excepts\r
1358           val ||= 'e'\r
1359           @supports[key] = val\r
1360         when :invex\r
1361           val ||= 'I'\r
1362           @supports[key] = val\r
1363         when :maxchannels\r
1364           noval_warn(key, val) {\r
1365             reparse += "CHANLIMIT=(chantypes):#{val} "\r
1366           }\r
1367         when :maxtargets\r
1368           noval_warn(key, val) {\r
1369             @supports[:targmax]['PRIVMSG'] = val.to_i\r
1370             @supports[:targmax]['NOTICE'] = val.to_i\r
1371           }\r
1372         when :network\r
1373           noval_warn(key, val) {\r
1374             @supports[key] = val\r
1375           }\r
1376         when :nicklen\r
1377           noval_warn(key, val) {\r
1378             @supports[key] = val.to_i\r
1379           }\r
1380         when :prefix\r
1381           if val\r
1382             val.scan(/\((.*)\)(.*)/) { |m, p|\r
1383               @supports[key][:modes] = m.scan(/./).map { |x| x.to_sym}\r
1384               @supports[key][:prefixes] = p.scan(/./).map { |x| x.to_sym}\r
1385             }\r
1386           else\r
1387             @supports[key][:modes] = nil\r
1388             @supports[key][:prefixes] = nil\r
1389           end\r
1390         when :safelist\r
1391           val_warn(key, val) {\r
1392             @supports[key] = val.nil? ? true : val\r
1393           }\r
1394         when :statusmsg\r
1395           noval_warn(key, val) {\r
1396             @supports[key] = val.scan(/./)\r
1397           }\r
1398         when :std\r
1399           noval_warn(key, val) {\r
1400             @supports[key] = val.split(',')\r
1401           }\r
1402         else\r
1403           @supports[key] =  val.nil? ? true : val\r
1404         end\r
1405       }\r
1406       reparse.gsub!("(chantypes)",@supports[:chantypes])\r
1407       parse_isupport(reparse) unless reparse.empty?\r
1408     end\r
1409 \r
1410     # Returns the casemap of the server.\r
1411     #\r
1412     def casemap\r
1413       @supports[:casemapping]\r
1414     end\r
1415 \r
1416     # Returns User or Channel depending on what _name_ can be\r
1417     # a name of\r
1418     #\r
1419     def user_or_channel?(name)\r
1420       if supports[:chantypes].include?(name[0])\r
1421         return Channel\r
1422       else\r
1423         return User\r
1424       end\r
1425     end\r
1426 \r
1427     # Returns the actual User or Channel object matching _name_\r
1428     #\r
1429     def user_or_channel(name)\r
1430       if supports[:chantypes].include?(name[0])\r
1431         return channel(name)\r
1432       else\r
1433         return user(name)\r
1434       end\r
1435     end\r
1436 \r
1437     # Checks if the receiver already has a channel with the given _name_\r
1438     #\r
1439     def has_channel?(name)\r
1440       channel_names.index(name.irc_downcase(casemap))\r
1441     end\r
1442     alias :has_chan? :has_channel?\r
1443 \r
1444     # Returns the channel with name _name_, if available\r
1445     #\r
1446     def get_channel(name)\r
1447       idx = has_channel?(name)\r
1448       channels[idx] if idx\r
1449     end\r
1450     alias :get_chan :get_channel\r
1451 \r
1452     # Create a new Channel object bound to the receiver and add it to the\r
1453     # list of <code>Channel</code>s on the receiver, unless the channel was\r
1454     # present already. In this case, the default action is to raise an\r
1455     # exception, unless _fails_ is set to false\r
1456     #\r
1457     def new_channel(name, topic=nil, users=[], fails=true)\r
1458       ex = get_chan(name)\r
1459       if ex\r
1460         raise "Channel #{name} already exists on server #{self}" if fails\r
1461         return ex\r
1462       else\r
1463 \r
1464         prefix = name[0].chr\r
1465 \r
1466         # Give a warning if the new Channel goes over some server limits.\r
1467         #\r
1468         # FIXME might need to raise an exception\r
1469         #\r
1470         warn "#{self} doesn't support channel prefix #{prefix}" unless @supports[:chantypes].include?(prefix)\r
1471         warn "#{self} doesn't support channel names this long (#{name.length} > #{@supports[:channellen]})" unless name.length <= @supports[:channellen]\r
1472 \r
1473         # Next, we check if we hit the limit for channels of type +prefix+\r
1474         # if the server supports +chanlimit+\r
1475         #\r
1476         @supports[:chanlimit].keys.each { |k|\r
1477           next unless k.include?(prefix)\r
1478           count = 0\r
1479           channel_names.each { |n|\r
1480             count += 1 if k.include?(n[0])\r
1481           }\r
1482           raise IndexError, "Already joined #{count} channels with prefix #{k}" if count == @supports[:chanlimit][k]\r
1483         }\r
1484 \r
1485         # So far, everything is fine. Now create the actual Channel\r
1486         #\r
1487         chan = Channel.new(name, topic, users, :server => self)\r
1488 \r
1489         # We wade through +prefix+ and +chanmodes+ to create appropriate\r
1490         # lists and flags for this channel\r
1491 \r
1492         @supports[:prefix][:modes].each { |mode|\r
1493           chan.create_mode(mode, Channel::UserMode)\r
1494         } if @supports[:prefix][:modes]\r
1495 \r
1496         @supports[:chanmodes].each { |k, val|\r
1497           if val\r
1498             case k\r
1499             when :typea\r
1500               val.each { |mode|\r
1501                 chan.create_mode(mode, Channel::ModeTypeA)\r
1502               }\r
1503             when :typeb\r
1504               val.each { |mode|\r
1505                 chan.create_mode(mode, Channel::ModeTypeB)\r
1506               }\r
1507             when :typec\r
1508               val.each { |mode|\r
1509                 chan.create_mode(mode, Channel::ModeTypeC)\r
1510               }\r
1511             when :typed\r
1512               val.each { |mode|\r
1513                 chan.create_mode(mode, Channel::ModeTypeD)\r
1514               }\r
1515             end\r
1516           end\r
1517         }\r
1518 \r
1519         @channels << chan\r
1520         # debug "Created channel #{chan.inspect}"\r
1521         return chan\r
1522       end\r
1523     end\r
1524 \r
1525     # Returns the Channel with the given _name_ on the server,\r
1526     # creating it if necessary. This is a short form for\r
1527     # new_channel(_str_, nil, [], +false+)\r
1528     #\r
1529     def channel(str)\r
1530       new_channel(str,nil,[],false)\r
1531     end\r
1532 \r
1533     # Remove Channel _name_ from the list of <code>Channel</code>s\r
1534     #\r
1535     def delete_channel(name)\r
1536       idx = has_channel?(name)\r
1537       raise "Tried to remove unmanaged channel #{name}" unless idx\r
1538       @channels.delete_at(idx)\r
1539     end\r
1540 \r
1541     # Checks if the receiver already has a user with the given _nick_\r
1542     #\r
1543     def has_user?(nick)\r
1544       user_nicks.index(nick.irc_downcase(casemap))\r
1545     end\r
1546 \r
1547     # Returns the user with nick _nick_, if available\r
1548     #\r
1549     def get_user(nick)\r
1550       idx = has_user?(nick)\r
1551       @users[idx] if idx\r
1552     end\r
1553 \r
1554     # Create a new User object bound to the receiver and add it to the list\r
1555     # of <code>User</code>s on the receiver, unless the User was present\r
1556     # already. In this case, the default action is to raise an exception,\r
1557     # unless _fails_ is set to false\r
1558     #\r
1559     def new_user(str, fails=true)\r
1560       tmp = str.to_irc_user(:server => self)\r
1561       old = get_user(tmp.nick)\r
1562       # debug "Tmp: #{tmp.inspect}"\r
1563       # debug "Old: #{old.inspect}"\r
1564       if old\r
1565         # debug "User already existed as #{old.inspect}"\r
1566         if tmp.known?\r
1567           if old.known?\r
1568             # debug "Both were known"\r
1569             # Do not raise an error: things like Freenode change the hostname after identification\r
1570             warning "User #{tmp.nick} has inconsistent Netmasks! #{self} knows #{old.inspect} but access was tried with #{tmp.inspect}" if old != tmp\r
1571             raise "User #{tmp} already exists on server #{self}" if fails\r
1572           end\r
1573           if old.fullform.downcase != tmp.fullform.downcase\r
1574             old.replace(tmp)\r
1575             # debug "Known user now #{old.inspect}"\r
1576           end\r
1577         end\r
1578         return old\r
1579       else\r
1580         warn "#{self} doesn't support nicknames this long (#{tmp.nick.length} > #{@supports[:nicklen]})" unless tmp.nick.length <= @supports[:nicklen]\r
1581         @users << tmp\r
1582         return @users.last\r
1583       end\r
1584     end\r
1585 \r
1586     # Returns the User with the given Netmask on the server,\r
1587     # creating it if necessary. This is a short form for\r
1588     # new_user(_str_, +false+)\r
1589     #\r
1590     def user(str)\r
1591       new_user(str, false)\r
1592     end\r
1593 \r
1594     # Deletes User _user_ from Channel _channel_\r
1595     #\r
1596     def delete_user_from_channel(user, channel)\r
1597       channel.delete_user(user)\r
1598     end\r
1599 \r
1600     # Remove User _someuser_ from the list of <code>User</code>s.\r
1601     # _someuser_ must be specified with the full Netmask.\r
1602     #\r
1603     def delete_user(someuser)\r
1604       idx = has_user?(someuser)\r
1605       raise "Tried to remove unmanaged user #{user}" unless idx\r
1606       have = self.user(someuser)\r
1607       @channels.each { |ch|\r
1608         delete_user_from_channel(have, ch)\r
1609       }\r
1610       @users.delete_at(idx)\r
1611     end\r
1612 \r
1613     # Create a new Netmask object with the appropriate casemap\r
1614     #\r
1615     def new_netmask(str)\r
1616       str.to_irc_netmask(:server => self)\r
1617     end\r
1618 \r
1619     # Finds all <code>User</code>s on server whose Netmask matches _mask_\r
1620     #\r
1621     def find_users(mask)\r
1622       nm = new_netmask(mask)\r
1623       @users.inject(UserList.new) {\r
1624         |list, user|\r
1625         if user.user == "*" or user.host == "*"\r
1626           list << user if user.nick.irc_downcase(casemap) =~ nm.nick.irc_downcase(casemap).to_irc_regexp\r
1627         else\r
1628           list << user if user.matches?(nm)\r
1629         end\r
1630         list\r
1631       }\r
1632     end\r
1633 \r
1634   end\r
1635 \r
1636 end\r
1637 \r