Anonymous avatar Anonymous committed c93db87

Version 0.1.0.

Comments (0)

Files changed (9)

+*.pyc
+		    GNU GENERAL PUBLIC LICENSE
+		       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+                       59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+			    Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+		    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+			    NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+		     END OF TERMS AND CONDITIONS
+
+	    How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) 19yy  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) 19yy name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Library General
+Public License instead of this License.
+1999-06-19  Joel Rosdahl  <joel@rosdahl.net>
+
+	* irclib.py: Released 0.1.0.
+VERSION := $(shell sed -n -e '/VERSION = /{s/VERSION = \(.*\), \(.*\), \(.*\)/\1.\2.\3/;p;}' <irclib.py)
+
+main:
+	echo "Nothing to be made."
+
+dist:
+	mkdir irclib-$(VERSION)
+	cp COPYING README ChangeLog Makefile irclib.py ircbot.py irccat servermap irclib-$(VERSION)
+	tar cvzf irclib-$(VERSION).tar.gz irclib-$(VERSION)
+	rm -r irclib-$(VERSION)
+
+cvstag:
+	cvs tag version_`echo $(VERSION) | sed 's/\./_/g'`
+
+clean:
+	rm -f *.tar.gz
+irclib.py
+---------
+
+This code is very beta, or not even that.  Use at your own risk and
+read the source, Luke!
+# THIS IS PROBABLY MOSTLY USELESS, ERRONEOUS, BUGGY AND UNTESTED CODE RIGHT NOW.
+#
+# Copyright (C) 1999 Joel Rosdahl
+# 
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#        
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+#
+# Joel Rosdahl <joel@rosdahl.net>
+#
+# $Id$
+
+import sys
+
+from irclib import *
+
+class SingleServerIRCBot:
+    def __init__(self, server_list, nickname, realname, channels_file="bot.channels"):
+        self.channels = {}
+        self.irc = IRC()
+        self.server_list = server_list
+        self.current_server = 0
+        self.channels_file = channels_file
+
+        try:
+            f = open(channels_file, "r")
+        except:
+            f = open(channels_file, "w")
+            f.close()
+            f = open(channels_file, "r")
+        for ch in f.readlines():
+            self.channels[ch[:-1]] = Channel(ch[:-1])
+        f.close()
+
+        self.nickname = nickname
+        self.realname = realname
+        self.connect()
+        for numeric in all_events:
+            self.connection.add_global_handler(numeric, self.event_dispatcher)
+
+    def connect(self):
+        password = None
+        if len(self.server_list[self.current_server]) > 2:
+            password = self.server_list[self.current_server][2]
+        self.connection = self.irc.server_connect(self.server_list[self.current_server][0],
+                                                  self.server_list[self.current_server][1],
+                                                  self.nickname,
+                                                  self.nickname,
+                                                  self.realname,
+                                                  password)
+
+    def jump_server(self):
+        self.get_connection().quit("Jumping servers")
+        self.current_server = (self.current_server + 1) % len(self.server_list)
+        self.connect()
+
+    def start(self):
+        self.irc.process_forever()
+
+    def get_connection(self):
+        return self.connection
+
+    def get_irc_object(self):
+        return self.ircobj
+
+    def join_new_channel(self, channel):
+        self.channels[channel] = Channel(channel)
+        self.get_connection().join(channel)
+        f = open(self.channels_file, "w")
+        f.writelines(map(lambda x: x+"\n", self.channels.keys()))
+        f.close()
+
+    def part_old_channel(self, channel):
+        self.get_connection().part(channel)
+        del self.channels[channel]
+        f = open(self.channels_file, "w")
+        f.writelines(map(lambda x: x+"\n", self.channels.keys()))
+        f.close()
+
+    def die(self):
+        self.connection.exit("I'll be back!")
+
+    def get_version(self):
+        return "IRCBot by Joel Rosdahl <joel@rosdahl.net>"
+
+    def event_dispatcher(self, c, e):
+        try:
+            method = getattr(self, "on_" + e.eventtype())
+        except AttributeError:
+            # No such handler.
+            return
+        apply(method, (c, e))
+
+    def on_welcome(self, c, e):
+        for ch in self.channels.keys():
+            c.join(ch)
+
+    def on_ctcp(self, c, e):
+        if e.arguments()[0] == "VERSION":
+            c.ctcp_reply(nick_from_nickmask(e.source()), self.get_version())
+        elif e.arguments()[0] == "PING":
+            if len(e.arguments()) > 1:
+                c.ctcp_reply(nick_from_nickmask(e.source()), "PING " + e.arguments()[1])
+    
+    def on_error(self, c, e):
+        self.jump_server()
+        # XXX join channels here, etc.
+        
+    def on_join(self, c, e):
+        self.channels[lower_irc_string(e.target())].add_nick(e.source())
+        if e.source() == c.get_nick_name():
+            self.channels[lower_irc_string(e.target())].set_joined()
+    
+    def on_kick(self, c, e):
+        if e.arguments()[0] == self.nickname:
+            self.channels[lower_irc_string(e.target())].clear_joined()
+            if self.channels[lower_irc_string(e.target())].auto_join():
+                def rejoin(c, channel):
+                    c.join(channel)
+                c.execute_delayed(10, rejoin, (c, e.target()))
+        else:
+            self.channels[lower_irc_string(e.target())].remove_nick(e.arguments()[0])
+    
+    def on_inviteonlychan(self, c, e):
+        # XXX
+        pass
+
+    def on_bannedfromchan(self, c, e):
+        # XXX
+        pass
+
+    def on_badchannelkey(self, c, e):
+        # XXX
+        pass
+
+    def on_namreply(self, c, e):
+        # e.arguments()[0] = "="     (why?)
+        # e.arguments()[1] = channel
+        # e.arguments()[2] = nick list
+
+        flags = ""
+        channel = lower_irc_string(e.arguments()[1])
+        for nick in string.split(e.arguments()[2]):
+            self.channels[channel].add_nick(nick)
+            if nick[0] == "@":
+                self.channels[channel].add_oper(nick[1:])
+            elif nick[0] == "+":
+                self.channels[channel].remove_oper(nick[1:])
+
+    def on_mode(self, c, e):
+        modes = parse_channel_modes(string.join(e.arguments()))
+        if is_channel(e.target()):
+            channel = lower_irc_string(e.target())
+            for mode in modes:
+                if mode[0] == "+":
+                    self.channels[channel].set_mode(mode[1], mode[2])
+                else:
+                    self.channels[channel].clear_mode(mode[1])
+        else:
+            # Mode on self... XXX
+            pass
+
+    def on_nick(self, c, e):
+        for channel in self.channels.values():
+            if e.source() in channel.users():
+                channel.change_nick(e.source(), e.target())
+        if nick_from_nickmask(e.source()) == self.nickname:
+            self.nickname = e.target()
+    
+    def on_part(self, c, e):
+        if nick_from_nickmask(e.source()) != self.nickname:
+            self.channels[lower_irc_string(e.target())].remove_nick(e.source())
+    
+    def on_privmsg(self, c, e):
+        pass
+    
+    def on_pubmsg(self, c, e):
+        pass
+    
+    def on_quit(self, c, e):
+        for channel in self.channels.values():
+            if e.source() in channel.users():
+                channel.remove_nick(e.source())
+
+
+class Channel:
+    def __init__(self, name, auto_join=1):
+        self.name = name
+        self.nicks = {}
+        self.opers = {}
+        self.voiced = {}
+        self._auto_join = auto_join
+        self.modes = {}
+        self.joined = 0
+
+    def get_name(self):
+        return self.name
+
+    def is_joined(self):
+        return self.joined
+
+    def set_joined(self):
+        self.joined = 1
+
+    def clear_joined(self):
+        self.joined = 0
+
+    def auto_join(self):
+        return self._auto_join
+
+    def users(self):
+        return self.nicks.keys()
+
+    def has_user(self, nick):
+        return self.nicks.has_keys(nick)
+
+    def is_oper(self, nick):
+        return self.opers.has_key(nick)
+
+    def is_voiced(self, nick):
+        return self.voiced.has_key(nick)
+
+    def add_nick(self, nick):
+        self.nicks[nick] = 1
+
+    def remove_nick(self, nick):
+        try:
+            del self.nicks[nick]
+            return 1
+        except KeyError:
+            return 0
+    
+    def add_oper(self, nick):
+        self.opers[nick] = 1
+
+    def remove_oper(self, nick):
+        try:
+            del self.opers[nick]
+            return 1
+        except KeyError:
+            return 0
+    
+    def add_voice(self, nick):
+        self.voiced[nick] = 1
+
+    def remove_voice(self, nick):
+        try:
+            del self.voiced[nick]
+            return 1
+        except KeyError:
+            return 0
+    
+    def change_nick(self, before, after):
+        self.nicks[after] = self.nicks[before]
+        del self.nicks[before]
+
+    def set_mode(self, mode, value=None):
+        self.modes[mode] = value
+
+    def clear_mode(self, mode, value=None):
+        if self.modes.has_key(mode):
+            del self.modes[mode]
+
+    def has_mode(self, mode):
+        return mode in self.modes
+
+    def is_moderated(self):
+        return self.has_mode("m")
+
+    def is_secret(self):
+        return self.has_mode("s")
+
+    def is_protected(self):
+        return self.has_mode("p")
+
+    def has_topic_lock(self):
+        return self.has_mode("t")
+
+    def is_invite_only(self):
+        return self.has_mode("i")
+
+    def has_message_from_outside_protection(self):
+        # Eh... What should it be called, really?
+        return self.has_mode("n")
+
+    def has_limit(self):
+        return self.has_mode("l")
+
+    def limit(self):
+        if self.has_limit():
+            return self.modes[l]
+        else:
+            return None
+
+    def has_key(self):
+        return self.has_mode("k")
+
+    def key(self):
+        if self.has_key():
+            return self.modes["k"]
+        else:
+            return None
+
+    def is_(self):
+        return self.has_mode("")
+
+    def is_(self):
+        return self.has_mode("")
+#! /usr/bin/env python
+#
+# Example script using irclib.py.
+#
+# Joel Rosdahl <joel@rosdahl.net>
+
+import irclib
+import string
+import sys
+
+def on_connect(connection, event):
+    if irclib.is_channel(target):
+        connection.join(target)
+    else:
+        while 1:
+            line = sys.stdin.readline()
+            if not line:
+                break
+            connection.privmsg(target, line)
+        connection.quit("Using irclib.py")
+
+def on_join(connection, event):
+    while 1:
+        line = sys.stdin.readline()
+        if not line:
+            break
+        connection.privmsg(target, line)
+    connection.quit("Using irclib.py")
+
+if len(sys.argv) != 4:
+    print "Usage: irccat <server[:port]> <nickname> <target>"
+    print "\ntarget is a nickname or a channel."
+    sys.exit(1)
+
+def on_disconnect(connection, event):
+    connection.exit()
+
+s = string.split(sys.argv[1], ":", 1)
+server = s[0]
+if len(s) == 2:
+    try:
+        port = int(s[1])
+    except ValueError:
+        print "Error: Erroneous port."
+        sys.exit(1)
+else:
+    port = 6667
+nickname = sys.argv[2]
+target = sys.argv[3]
+
+irc = irclib.IRC()
+try:
+    c = irc.server_connect(server, port, nickname, nickname, nickname)
+except irclib.ServerConnectionError, x:
+    print x
+    sys.exit(1)
+
+c.add_global_handler("welcome", on_connect)
+c.add_global_handler("join", on_join)
+c.add_global_handler("disconnect", on_disconnect)
+
+irc.process_forever()
+# irclib -- IRC protocol client library.
+
+# irclib -- IRC protocol client library
+#
+# Copyright (C) 1999 Joel Rosdahl
+# 
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#        
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+#
+# Joel Rosdahl <joel@rosdahl.net>
+#
+# $Id$
+
+"""irclib -- IRC protocol client library.
+
+This is intended to encapsulate the IRC protocol at a quite low
+level.  Clients are written by registering callbacks (i.e. not by
+inheriting class), to make it able to use different programming
+paradigms easily.  (It should be quite easy to write an
+object-oriented wrapper for one server connection, for example.)
+
+The class hierarchy is inspired by the Perl IRC module (Net::IRC).
+
+This is mostly alpha software right now.
+
+Joel Rosdahl <joel@rosdahl.net>
+"""
+
+import bisect
+import re
+import select
+import socket
+import string
+import sys
+import time
+import types
+
+VERSION = 0, 1, 0
+DEBUG = 0
+
+# TODO
+# ----
+# DCC
+# (maybe) thread safety
+# (maybe) color parser convenience functions
+# documentation (including all event types)
+# (maybe) add awareness of different types of ircds
+
+# NOTES
+# -----
+# connection.quit() only sends QUIT to the server.
+# ERROR from the server triggers the error event and the disconnect event.
+# dropping of the connection triggers the disconnect event.
+# exit()
+
+class IRCError(Exception):
+    pass
+
+class IRC:
+    def __init__(self, fn_to_add_socket=None,
+                 fn_to_remove_socket=None,
+                 fn_to_add_timeout=None):
+        """Creates an IRC object.
+
+        Optional arguments are fn_to_add_socket, fn_to_remove_socket
+        and fn_to_add_timeout.  The first two specify functions that
+        will be called with a socket object as argument when the IRC
+        object wants to be notified (or stop being notified) of data
+        coming on a new socket.  When new data arrives, the method
+        process_data should be called.  Similarly, fn_to_add_timeout
+        is called with a number of seconds (a floating point number)
+        as first argument when the IRC object wants to receive a
+        notification (by calling the process_timeout method).  So, if
+        e.g. the argument is 42.17, the object wants the
+        process_timeout method to be called after 42 seconds and 170
+        milliseconds.
+
+        The three arguments mainly exist to be able to use an external
+        main loop (for example Tkinter's or PyGTK's main app loop)
+        instead of calling the process_forever method.
+        """
+
+        if fn_to_add_socket and fn_to_remove_socket:
+            self.fn_to_add_socket = fn_to_add_socket
+            self.fn_to_remove_socket = fn_to_remove_socket
+        else:
+            self.fn_to_add_socket = None
+            self.fn_to_remove_socket = None
+
+        self.fn_to_add_timeout = fn_to_add_timeout
+        self.connections = []
+        self.handlers = {}
+        self.delayed_commands = [] # list of (time, function, arguments)
+
+        self.add_global_handler("ping", _ping_ponger, -42)
+
+    def server_connect(self, server, port, nick, username, ircname, password=None):
+        """Connect to an IRC server.
+
+        Returns a ServerConnection object.
+        """
+
+        c = ServerConnection(self, server, port, nick, username, ircname, password)
+        self.connections.append(c)
+        if self.fn_to_add_socket:
+            self.fn_to_add_socket(c._get_socket())
+        return c
+
+    def DCC_connect(self, host, port):
+        raise IRCError, "Unimplemented."
+        c = DCCConnection(self, host, port)
+
+    def process_data(self, sockets):
+        """Called when there is more data to read.
+
+        The argument is a list of socket objects.
+        """
+        for s in sockets:
+           for c in self.connections:
+               if s == c._get_socket():
+                   c.process_data()
+
+    def process_timeout(self):
+        """Called when a timeout notification is due."""
+        t = time.time()
+        while self.delayed_commands:
+            if t >= self.delayed_commands[0][0]:
+                apply(self.delayed_commands[0][1], self.delayed_commands[0][2])
+                del self.delayed_commands[0]
+            else:
+                break
+
+    def process_forever(self):
+        """This starts an infinite loop, processing data from connections.
+
+        Timeouts will be processed every second.
+        """
+        while 1:
+            (i, o, e) = select.select(map(lambda x: x._get_socket(),
+                                          self.connections),
+                                      [],
+                                      [],
+                                      1)
+            self.process_data(i)
+            self.process_timeout()
+
+    def exit(self, message=""):
+        """Quits all connections and exits the program."""
+        for c in self.connections:
+            if message:
+                c.quit(message)
+            c.disconnect(message)
+        sys.exit(0)
+
+    def add_global_handler(self, event, handler, priority=0):
+        """Adds a global handler function for a type of event.
+
+        The function is called whenever the specified event is
+        triggered in any of the connections.
+
+        The handler functions are called in priority order (lowest
+        number is highest priority).  If a handler function returns
+        "NO MORE", no more handlers will be called.
+
+        Arguments:
+            event -- Event type (a string).
+            handler -- Callback function.
+            priority -- A number (the lower number, the higher priority).
+        """
+
+        if not self.handlers.has_key(event):
+            self.handlers[event] = []
+        bisect.insort(self.handlers[event], ((priority, handler)))
+##         self.handlers[event].append((priority, handler))
+##         self.handlers[event].sort()
+
+    def remove_global_handler(self, event, handler):
+        """Removes a global handler function.
+
+        Returns 1 on success, otherwise 0.
+
+        Arguments:
+            event -- Event type (a string).
+            handler -- Callback function.
+        """
+        if not self.handlers.has_key(event):
+            return 0
+        for h in self.handlers[event]:
+            if handler == h[1]:
+                self.handlers[event].remove(h)
+        return 1
+
+    def execute_at(self, at, function, arguments):
+        self.execute_delayed(at-time.time(), function, arguments)
+
+    def execute_delayed(self, delay, function, arguments):
+        bisect.insort(self.delayed_commands, (delay+time.time(), function, arguments))
+        if self.fn_to_add_timeout:
+            self.fn_to_add_timeout(delay)
+
+    def _handle_event(self, connection, event):
+       if self.handlers.has_key(event.eventtype()):
+           for handler in self.handlers[event.eventtype()]:
+               if handler[1](connection, event) == "NO MORE":
+                   return
+
+    def _remove_connection(self, connection):
+        self.connections.remove(connection)
+        if self.fn_to_remove_socket:
+            self.fn_to_remove_socket(connection._get_socket()) 
+
+_rfc_1459_command_regexp = re.compile("^(:(?P<prefix>[^ ]+) +)?(?P<command>[^ ]+)( +(?P<argument>.+))?")
+
+class Connection:
+    def __init__(self, irclibobj):
+        self.irclibobj = irclibobj
+
+    def _get_socket():
+        raise IRCError, "Not overridden"
+
+    def execute_at(self, at, function, arguments):
+        self.irclibobj.execute_at(at, function, arguments)
+        
+    def execute_delayed(self, delay, function, arguments):
+        self.irclibobj.execute_delayed(delay, function, arguments)
+
+class ServerConnectionError(IRCError):
+    pass
+
+class ServerConnection(Connection):
+    # Creates a ServerConnection object.
+    def __init__(self, irclibobj, server, port, nickname, username, ircname, password):
+        Connection.__init__(self, irclibobj)
+        self.server = server
+        self.port = port
+        self.nickname = nickname
+        self.username = username
+        self.ircname = ircname
+        self.password = password
+        self.connected = 0  # Not connected yet.
+        self.socket = None
+        self.previous_buffer = ""
+        self.handlers = {}
+        self.real_server_name = ""
+        self.real_nickname = nickname
+
+        self.connect_to_server()
+
+    def connect_to_server(self):
+        """(Re)connect to server.
+
+        This function can be called to reconnect a closed connection.
+        """
+        if self.connected:
+            self.quit("Changing server")
+        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        try:
+            self.socket.connect(self.server, self.port)
+        except socket.error, x:
+            raise ServerConnectionError, "Couldn't connect to socket: %s" % x
+        self.connected = 1
+
+        # Log on...
+        if self.password:
+            self.pass_(self.password)
+        self.nick(self.nickname)
+        self.user(self.username, self.ircname)
+
+    def _get_socket(self):
+        return self.socket
+
+    def get_server_name(self):
+        """Get server name."""
+        if self.real_server_name:
+            return self.real_server_name
+        else:
+            raise ServerConnectionError, "Not connected yet"
+
+    def get_nick_name(self):
+        return self.real_nickname
+
+    def process_data(self):
+        try:
+            new_data = self.socket.recv(2**14)
+        except socket.error, x:
+            # The server hung up.
+            self._handle_event(Event("disconnect",
+                                     self.get_server_name(),
+                                     None,
+                                     []))
+            return
+        if not new_data:
+            # Read nothing: connection must be down.
+            self.disconnect("Connection reset by peer")
+            return
+          
+        lines = string.split(self.previous_buffer + new_data, "\r\n")
+
+        # Huh!?  Crrrrazy EFNet doesn't follow the RFC: their ircd
+        # seems to use \n as message separator!  :P
+        efnet_kluge = string.split(self.previous_buffer + new_data, "\n")
+        if len(efnet_kluge) > len(lines):
+            lines = efnet_kluge
+
+        # Save the last, unfinished line.
+        self.previous_buffer = lines[-1]
+        lines = lines[:-1]
+
+        for line in lines:
+            prefix = None
+            command = None
+            arguments = None
+            try:
+                self._handle_event(Event("allrawmessages",
+                                         self.get_server_name(),
+                                         None,
+                                         [line]))
+            except ServerConnectionError:
+                pass
+
+            m = _rfc_1459_command_regexp.match(line)
+            if m.group("prefix"):
+                prefix = m.group("prefix")
+                if not self.real_server_name:
+                    self.real_server_name = prefix
+
+            if m.group("command"):
+                command = string.lower(m.group("command"))
+
+            if m.group("argument"):
+                a = string.split(m.group("argument"), ":", 1) # XXX ":" can be included in arguments, but not as first character!
+                arguments = string.split(a[0])
+                if len(a) == 2:
+                    arguments.append(a[1])
+       
+
+            if command == "nick":
+                if nick_from_nickmask(prefix) == self.real_nickname:
+                    self.real_nickname = nick_from_nickmask(prefix)
+
+            if command in ["privmsg", "notice"]:
+                target, message = arguments[0], arguments[1]
+                messages = _ctcp_dequote(message)
+
+                if command == "privmsg":
+                    if is_channel(target):
+                        command = "pubmsg"
+                else:
+                    if is_channel(target):
+                        command = "pubnotice"
+                    else:
+                        command = "privnotice"
+
+                for m in messages:
+                    if type(m) is types.TupleType:
+                        if command in ["privmsg", "pubmsg"]:
+                            command = "ctcp"
+                        else:
+                            command = "ctcpreply"
+
+                        m = list(m)
+                        if DEBUG:
+                            print "command: %s, source: %s, target: %s, arguments: %s" % (
+                                command, prefix, target, m)
+                        self._handle_event(Event(command, prefix, target, m))
+                    else:
+                        if DEBUG:
+                            print "command: %s, source: %s, target: %s, arguments: %s" % (
+                                command, prefix, target, [m])
+                        self._handle_event(Event(command, prefix, target, [m]))
+            else:
+                target = None
+  
+                if command == "quit":
+                    arguments = [arguments[0]]
+                elif command == "ping":
+                    target = arguments[0]
+                else:
+                    target = arguments[0]
+                    arguments = arguments[1:]
+
+                if command == "mode":
+                    if not is_channel(target):
+                        command = "umode"
+
+                # Translate numerics into more readable strings.
+                if numeric_events.has_key(command):
+                    command = numeric_events[command]
+  
+                if DEBUG:
+                    print "command: %s, source: %s, target: %s, arguments: %s" % (
+                        command, prefix, target, arguments)
+                self._handle_event(Event(command, prefix, target, arguments))
+
+    def _handle_event(self, event):
+        self.irclibobj._handle_event(self, event)
+        if self.handlers.has_key(event.eventtype()):
+            for fn in self.handlers[event.eventtype()]:
+                fn(self, event)
+
+    def add_global_handler(self, *args):
+        """See documentation for IRC.add_global_handler."""
+        apply(self.irclibobj.add_global_handler, args)
+
+##     def add_handler(self, event, fn, location="replace", global_handler=0):
+##         if global_handler:
+##             self.irclibobj.add_global_handler(event, fn, location)
+##         else:
+##             self._add_handler(event, fn, location)
+
+##     def _add_handler(self, event, fn, location):
+##         if location == "replace":
+##             self.handlers[event] = [fn]
+##         elif location == "before":
+##             if not self.handlers.has_key(event):
+##                 self.handlers[event] = []
+##             self.handlers[event][0:0] = [fn]
+##         elif location == "after":
+##             if not self.handlers.has_key(event):
+##                 self.handlers[event] = []
+##             self.handlers[event].append(fn)
+##         else:
+##             raise "Strange location specified."
+        
+    def action(self, target, action):
+        self.ctcp("ACTION", target, action)
+
+    def admin(self, server=""):
+        self.send_raw(string.strip(string.join(["ADMIN", server])))
+
+    def connect(self, target, port="", server=""):
+        self.send_raw("CONNECT %s%s%s" % (target,
+                                          port and (" " + port),
+                                          server and (" " + server)))
+
+    def ctcp(self, ctcptype, target, parameter=""):
+        ctcptype = string.upper(ctcptype)
+        self.privmsg(target, "\001%s%s\001" % (ctcptype, parameter and (" " + parameter) or ""))
+
+    def ctcp_reply(self, target, parameter):
+        self.notice(target, "\001%s\001" % parameter)
+
+    def disconnect(self, message=""):
+        """Hang up the connection."""
+        self.connected = 0
+        self.irclibobj._remove_connection(self)
+        try:
+            self.socket.close()
+        except socket.error, x:
+            pass
+        self._handle_event(Event("disconnect", self.server, "", [message]))
+
+    def exit(self, message=""):
+        """Quits all connections and exits the program."""
+        self.irclibobj.exit(message)
+
+    def globops(self, text):
+        self.send_raw("GLOBOPS :" + text)
+
+    def info(self, server=""):
+        self.send_raw(string.strip(string.join(["INFO", server])))
+
+    def invite(self, nick, channel):
+        self.send_raw(string.strip(string.join(["INVITE", nick, channel])))
+
+    def ison(self, nicks):
+        """nicks is a list of nicks"""
+        self.send_raw("ISON " + string.join(nicks, ","))
+
+    def join(self, channel, key=""):
+        self.send_raw("JOIN %s%s" % (channel, (key and (" " + key))))
+
+    def kick(self, channel, nick, comment=""):
+        self.send_raw("KICK %s %s%s" % (channel, nick, (comment and (" :" + comment))))
+
+    def links(self, remote_server="", server_mask=""):
+        command = "LINKS"
+        if remote_server:
+            command = command + " " + remote_server
+        if server_mask:
+            command = command + " " + server_mask
+        self.send_raw(command)
+
+    def list(self, channels=None, server=""):
+        command = "LIST"
+        if channels:
+            command = command + " " + string.join(channels, ",")
+        if server:
+            command = command + " " + server
+        self.send_raw(command)
+
+    def lusers(self, server=""):
+        self.send_raw("LUSERS" + (server and (" " + server)))
+
+    def mode(self, target, command):
+        self.send_raw("MODE %s %s" % (target, command))
+
+    def motd(self, server=""):
+        self.send_raw("MOTD" + (server and (" " + server)))
+
+    def names(self, channels=None):
+        self.send_raw("NAMES" + (channels and (" " + string.join(channels, ",")) or ""))
+
+    def nick(self, newnick):
+        self.send_raw("NICK " + newnick)
+
+    def notice(self, target, text):
+        # Should limit len(text) here!
+        self.send_raw("NOTICE %s :%s" % (target, text))
+
+    def oper(self, nick, password):
+        self.send_raw("OPER %s %s" % (nick, password))
+
+    def part(self, channels):
+        if type(channels) == types.StringType:
+            self.send_raw("PART " + channels)
+        else:
+            self.send_raw("PART " + string.join(channels, ","))
+
+    def pass_(self, password):
+        self.send_raw("PASS " + password)
+
+    def ping(self, target, target2=""):
+       self.send_raw("PING %s%s" % (target, target2 and (" " + target2)))
+
+    def pong(self, target, target2=""):
+       self.send_raw("PONG %s%s" % (target, target2 and (" " + target2)))
+
+    def privmsg(self, target, text):
+        # Should limit len(text) here!
+        self.send_raw("PRIVMSG %s :%s" % (target, text))
+
+    def privmsg_many(self, targets, text):
+        # Should limit len(text) here!
+        self.send_raw("PRIVMSG %s :%s" % (string.join(targets, ","), text))
+
+    def quit(self, message=""):
+        self.send_raw("QUIT" + (message and (" :" + message)))
+
+    def send_raw(self, string):
+        """Send raw string to server.
+
+        The string will be padded with appropriate CR LF.
+        """
+        try:
+            self.socket.send(string + "\r\n")
+            if DEBUG:
+                print "SENT TO SERVER:", string
+        except socket.error, x:
+            # Aouch!
+            self.disconnect("Connection reset by peer.")
+
+    def squit(self, server, comment=""):
+        self.send_raw("SQUIT %s%s" % (server, comment and (" :" + comment)))
+
+    def stats(self, statstype, server=""):
+        self.send_raw("STATS %s%s" % (statstype, server and (" " + server)))
+
+    def time(self, server=""):
+        self.send_raw("TIME" + (server and (" " + server)))
+
+    def topic(self, channel, new_topic=None):
+        if new_topic == None:
+            self.send_raw("TOPIC " + channel)
+        else:
+            self.send_raw("TOPIC %s :%s" % (channel, new_topic))
+
+    def trace(self, target=""):
+        self.send_raw("TRACE" + (target and (" " + target)))
+
+    def user(self, username, ircname):
+        self.send_raw("USER %s * * :%s" % (username, ircname))
+
+    def userhost(self, nicks):
+        self.send_raw("USERHOST " + string.join(nicks, ","))
+
+    def users(self, server=""):
+        self.send_raw("USERS" + (server and (" " + server)))
+
+    def version(self, server=""):
+        self.send_raw("VERSION" + (server and (" " + server)))
+        
+    def wallops(self, text):
+        self.send_raw("WALLOPS :" + text)
+
+    def who(self, target="", op=""):
+        self.send_raw("WHO%s%s" % (target and (" " + target), op and (" o")))
+
+    def whois(self, targets):
+        self.send_raw("WHOIS " + string.join(targets, ","))
+
+    def whowas(self, nick, max=None, server=""):
+        self.send_raw("WHOWAS %s%s%s" % (nick,
+                                         max and (" " + max),
+                                         server and (" " + server)))
+
+class DCCConnection(Connection):
+    # XXX
+    def __init__(self, irclibobj, host, port):
+        self.irclibobj = irclibobj
+        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        self.socket.connect(self.server, self.port)
+
+    def _get_socket(self):
+        return self.socket
+
+    def process_data(self):
+        pass
+
+class Event:
+    """Event class
+
+    Arguments:
+    eventtype -- A string describing the event.
+    source -- The originator of the event (a nick mask or a server). XXX Correct?
+    target -- The target of the event (a nick or a channel). XXX Correct?
+    arguments -- Any event specific arguments.
+    """
+    def __init__(self, eventtype, source, target, arguments=None):
+        self._eventtype = eventtype
+        self._source = source
+        self._target = target
+        if arguments:
+            self._arguments = arguments
+        else:
+            self._arguments = []
+
+    def eventtype(self):
+       return self._eventtype
+
+    def source(self):
+       return self._source
+
+    def target(self):
+       return self._target
+
+    def arguments(self):
+       return self._arguments
+
+_LOW_LEVEL_QUOTE = "\020"
+_CTCP_LEVEL_QUOTE = "\134"
+_CTCP_DELIMITER = "\001"
+
+_low_level_mapping = {
+    "0": "\000",
+    "n": "\n",
+    "r": "\r",
+    _LOW_LEVEL_QUOTE: _LOW_LEVEL_QUOTE
+}
+
+_low_level_regexp = re.compile(_LOW_LEVEL_QUOTE + "(.)")
+
+def mask_matches(nick, mask):
+    nick = lower_irc_string(nick)
+    mask = lower_irc_string(mask)
+    mask = string.replace(mask, "\\", "\\\\")
+    for ch in ".$|[](){}?+":
+        mask = string.replace(mask, ch, "\\" + ch)
+    mask = string.replace(mask, "?", ".")
+    mask = string.replace(mask, "*", ".*")
+    r = re.compile(mask, re.IGNORECASE)
+    return r.match(nick)
+
+def lower_irc_string(s):
+    s = string.lower(s)
+    s = string.replace(s, "[", "{")
+    s = string.replace(s, "\\", "|")
+    s = string.replace(s, "]", "}")
+    s = string.replace(s, "^", "~")
+    return s
+
+def _ctcp_dequote(message):
+    """Dequote a message according to CTCP specifications.
+
+    The function returns a list where each element can be either a
+    string (normal message) or a tuple of one or two strings (tagged
+    messages).  If a tuple has only one element (ie is a singleton),
+    that element is the tag; otherwise the tuple has two elements: the
+    tag and the data.
+
+    Arguments:
+        message -- The message to be decoded.
+    """
+
+    def _low_level_replace(match_obj):
+        ch = match_obj.group(1)
+
+        # If low_level_mapping doesn't have the character as key, we
+        # should just return the character.
+        return _low_level_mapping.get(ch, ch)
+
+    if _LOW_LEVEL_QUOTE in message:
+        # Yup, there was a quote.  Release the dequoter, man!
+        message = _low_level_regexp.sub(_low_level_replace, message)
+
+    if _CTCP_DELIMITER not in message:
+        return [message]
+    else:
+        # Split it into parts.  (Does any IRC client actually *use*
+        # CTCP stacking like this?)
+        chunks = string.split(message, _CTCP_DELIMITER)
+
+        messages = []
+        i = 0
+        while i < len(chunks)-1:
+            # Add message if it's non-empty.
+            if len(chunks[i]) > 0:
+                messages.append(chunks[i])
+
+            if i < len(chunks)-2:
+                # Aye!  CTCP tagged data ahead!
+                messages.append(tuple(string.split(chunks[i+1], " ", 1)))
+
+            i = i + 2
+
+        if len(chunks) % 2 == 0:
+            # Hey, a lonely _CTCP_DELIMITER at the end!  This means
+            # that the last chunk, including the delimiter, is a
+            # normal message!  (This is according to the CTCP
+            # specification.)
+            messages.append(_CTCP_DELIMITER + chunks[-1])
+        
+        return messages
+
+def is_channel(string):
+   return string and string[0] in "#&+"
+
+def nick_from_nickmask(s):
+    return string.split(s, "!")[0]
+
+def userhost_from_nickmask(s):
+    return string.split(s, "!")[1]
+
+def host_from_nickmask(s):
+    return string.split(s, "@")[1]
+
+def user_from_nickmask(s):
+    s = string.split(s, "!")[1]
+    return string.split(s, "@")[0]
+
+def parse_nick_modes(mode_string):
+    return _parse_modes(mode_string, "")
+
+def parse_channel_modes(mode_string):
+    return _parse_modes(mode_string, "bklvo")
+
+def _parse_modes(mode_string, unary_modes=""):
+    modes = []
+    arg_count = 0
+
+    # State variable.
+    sign = ""
+
+    a = string.split(mode_string)
+    if len(a) == 0:
+        return []
+    else:
+        mode_part, args = a[0], a[1:]
+
+    if mode_part[0] not in "+-":
+        return []
+    for ch in mode_part:
+        if ch in "+-":
+            sign = ch
+        elif ch == " ":
+            collecting_arguments = 1
+        elif ch in unary_modes:
+            modes.append([sign, ch, args[arg_count]])
+            arg_count = arg_count + 1
+        else:
+            modes.append([sign, ch, None])
+    return modes
+
+def _ping_ponger(connection, event):
+    connection.pong(event.target())
+
+nick_characters = "]ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvxyz0123456789\\[-`^{}"
+
+# Numeric table mostly stolen from the Perl IRC module (Net::IRC).
+numeric_events = {
+    "001": "welcome",
+    "002": "yourhost",
+    "003": "created",
+    "004": "myinfo",
+    "005": "featurelist",  # XXX
+    "200": "tracelink",
+    "201": "traceconnecting",
+    "202": "tracehandshake",
+    "203": "traceunknown",
+    "204": "traceoperator",
+    "205": "traceuser",
+    "206": "traceserver",
+    "208": "tracenewtype",
+    "209": "traceclass",
+    "211": "statslinkinfo",
+    "212": "statscommands",
+    "213": "statscline",
+    "214": "statsnline",
+    "215": "statsiline",
+    "216": "statskline",
+    "217": "statsqline",
+    "218": "statsyline",
+    "219": "endofstats",
+    "221": "umodeis",
+    "231": "serviceinfo",
+    "232": "endofservices",
+    "233": "service",
+    "234": "servlist",
+    "235": "servlistend",
+    "241": "statslline",
+    "242": "statsuptime",
+    "243": "statsoline",
+    "244": "statshline",
+    "250": "luserconns",
+    "251": "luserclient",
+    "252": "luserop",
+    "253": "luserunknown",
+    "254": "luserchannels",
+    "255": "luserme",
+    "256": "adminme",
+    "257": "adminloc1",
+    "258": "adminloc2",
+    "259": "adminemail",
+    "261": "tracelog",
+    "262": "endoftrace",
+    "265": "n_local",
+    "266": "n_global",
+    "300": "none",
+    "301": "away",
+    "302": "userhost",
+    "303": "ison",
+    "305": "unaway",
+    "306": "nowaway",
+    "311": "whoisuser",
+    "312": "whoisserver",
+    "313": "whoisoperator",
+    "314": "whowasuser",
+    "315": "endofwho",
+    "316": "whoischanop",
+    "317": "whoisidle",
+    "318": "endofwhois",
+    "319": "whoischannels",
+    "321": "liststart",
+    "322": "list",
+    "323": "listend",
+    "324": "channelmodeis",
+    "329": "channelcreate",
+    "331": "notopic",
+    "332": "topic",
+    "333": "topicinfo",
+    "341": "inviting",
+    "342": "summoning",
+    "351": "version",
+    "352": "whoreply",
+    "353": "namreply",
+    "361": "killdone",
+    "362": "closing",
+    "363": "closeend",
+    "364": "links",
+    "365": "endoflinks",
+    "366": "endofnames",
+    "367": "banlist",
+    "368": "endofbanlist",
+    "369": "endofwhowas",
+    "371": "info",
+    "372": "motd",
+    "373": "infostart",
+    "374": "endofinfo",
+    "375": "motdstart",
+    "376": "endofmotd",
+    "377": "motd2",        # 1997-10-16 -- tkil
+    "381": "youreoper",
+    "382": "rehashing",
+    "384": "myportis",
+    "391": "time",
+    "392": "usersstart",
+    "393": "users",
+    "394": "endofusers",
+    "395": "nousers",
+    "401": "nosuchnick",
+    "402": "nosuchserver",
+    "403": "nosuchchannel",
+    "404": "cannotsendtochan",
+    "405": "toomanychannels",
+    "406": "wasnosuchnick",
+    "407": "toomanytargets",
+    "409": "noorigin",
+    "411": "norecipient",
+    "412": "notexttosend",
+    "413": "notoplevel",
+    "414": "wildtoplevel",
+    "421": "unknowncommand",
+    "422": "nomotd",
+    "423": "noadmininfo",
+    "424": "fileerror",
+    "431": "nonicknamegiven",
+    "432": "erroneusnickname", # Thiss iz how its speld in thee RFC.
+    "433": "nicknameinuse",
+    "436": "nickcollision",
+    "441": "usernotinchannel",
+    "442": "notonchannel",
+    "443": "useronchannel",
+    "444": "nologin",
+    "445": "summondisabled",
+    "446": "usersdisabled",
+    "451": "notregistered",
+    "461": "needmoreparams",
+    "462": "alreadyregistered",
+    "463": "nopermforhost",
+    "464": "passwdmismatch",
+    "465": "yourebannedcreep", # I love this one...
+    "466": "youwillbebanned",
+    "467": "keyset",
+    "471": "channelisfull",
+    "472": "unknownmode",
+    "473": "inviteonlychan",
+    "474": "bannedfromchan",
+    "475": "badchannelkey",
+    "476": "badchanmask",
+    "481": "noprivileges",
+    "482": "chanoprivsneeded",
+    "483": "cantkillserver",
+    "491": "nooperhost",
+    "492": "noservicehost",
+    "501": "umodeunknownflag",
+    "502": "usersdontmatch",
+}
+
+generated_events = [
+    # Generated events
+    "disconnect",
+    "ctcp",
+    "ctcpreply"
+]
+
+protocol_events = [
+    # IRC protocol events
+    "error",
+    "join",
+    "kick",
+    "mode",
+    "part",
+    "ping",
+    "privmsg",
+    "privnotice",
+    "pubmsg",
+    "pubnotice",
+    "quit"
+]
+
+all_events = generated_events + protocol_events + numeric_events.values()
+#! /usr/bin/env python
+#
+# Example script using irclib.py.
+#
+# Joel Rosdahl <joel@rosdahl.net>
+
+import irclib
+import string
+import sys
+
+if len(sys.argv) != 3:
+    print "Usage: servermap <server[:port]> <nickname>"
+    sys.exit(1)
+
+links = []
+
+def on_connect(connection, event):
+    sys.stdout.write("\nGetting links...")
+    sys.stdout.flush()
+    connection.links()
+
+def on_passwdmismatch(connection, event):
+    print "Password required."
+    sys.exit(1)
+
+def on_links(connection, event):
+    global links
+
+    links.append((event.arguments()[0],
+                  event.arguments()[1],
+                  event.arguments()[2]))
+
+def on_endoflinks(connection, event):
+    global links
+
+    print "\n"
+
+    m = {}
+    for (to_node, from_node, desc) in links:
+        if from_node != to_node:
+            m[from_node] = m.get(from_node, []) + [to_node]
+
+    if m.has_key(connection.get_server_name()):
+        if len(m[connection.get_server_name()]) == 1:
+            hubs = len(m) - 1
+        else:
+            hubs = len(m)
+    else:
+        hubs = 0
+    
+    print "%d servers (%d leaves and %d hubs)\n" % (len(links), len(links)-hubs, hubs)
+
+    print_tree(0, [], connection.get_server_name(), m)
+    connection.quit("Using irclib.py")
+
+def on_disconnect(connection, event):
+    connection.exit()
+
+def indent_string(level, active_levels, last):
+    if level == 0:
+        return ""
+    s = ""
+    for i in range(level-1):
+        if i in active_levels:
+            s = s + "| "
+        else:
+            s = s + "  "
+    if last:
+        s = s + "`-"
+    else:
+        s = s + "|-"
+    return s
+
+def print_tree(level, active_levels, root, map, last=0):
+    sys.stdout.write(indent_string(level, active_levels, last)
+                     + root + "\n")
+    if map.has_key(root):
+        list = map[root]
+        for r in list[:-1]:
+            print_tree(level+1, active_levels[:]+[level], r, map)
+        print_tree(level+1, active_levels[:], list[-1], map, 1)
+        
+s = string.split(sys.argv[1], ":", 1)
+server = s[0]
+if len(s) == 2:
+    try:
+        port = int(s[1])
+    except ValueError:
+        print "Error: Erroneous port."
+        sys.exit(1)
+else:
+    port = 6667
+nickname = sys.argv[2]
+
+irc = irclib.IRC()
+sys.stdout.write("Connecting to server...")
+sys.stdout.flush()
+try:
+    c = irc.server_connect(server, port, nickname, nickname, nickname)
+except irclib.ServerConnectionError, x:
+    print x
+    sys.exit(1)
+
+c.add_global_handler("welcome", on_connect)
+c.add_global_handler("passwdmismatch", on_passwdmismatch)
+c.add_global_handler("links", on_links)
+c.add_global_handler("endoflinks", on_endoflinks)
+c.add_global_handler("disconnect", on_disconnect)
+
+irc.process_forever()
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.