]> code.delx.au - pymsnt/commitdiff
Reimporting (0.9.5)
authorjamesbunton <jamesbunton@55fbd22a-6204-0410-b2f0-b6c764c7e90a>
Mon, 31 Oct 2005 00:47:55 +0000 (00:47 +0000)
committerjamesbunton <jamesbunton@55fbd22a-6204-0410-b2f0-b6c764c7e90a>
Mon, 31 Oct 2005 00:47:55 +0000 (00:47 +0000)
git-svn-id: http://delx.cjb.net/svn/pymsnt/trunk@1 55fbd22a-6204-0410-b2f0-b6c764c7e90a

committer: jamesbunton <jamesbunton@55fbd22a-6204-0410-b2f0-b6c764c7e90a>

35 files changed:
COPYING [new file with mode: 0644]
PyMSNt [new file with mode: 0755]
README [new file with mode: 0644]
TODO [new file with mode: 0644]
config-example.xml [new file with mode: 0644]
src/baseproto/__init__.py [new file with mode: 0644]
src/baseproto/glue.py [new file with mode: 0644]
src/config.py [new file with mode: 0644]
src/debug.py [new file with mode: 0644]
src/disco.py [new file with mode: 0644]
src/groupchat.py [new file with mode: 0644]
src/jabw.py [new file with mode: 0644]
src/lang.py [new file with mode: 0644]
src/legacy/__init__.py [new file with mode: 0644]
src/legacy/glue.py [new file with mode: 0644]
src/legacy/msnw.py [new file with mode: 0644]
src/legacy/subscription.py [new file with mode: 0644]
src/main.py [new file with mode: 0644]
src/misciq.py [new file with mode: 0644]
src/register.py [new file with mode: 0644]
src/session.py [new file with mode: 0644]
src/tlib/__init__.py [new file with mode: 0644]
src/tlib/domish.py [new file with mode: 0644]
src/tlib/jabber/__init__.py [new file with mode: 0644]
src/tlib/jabber/client.py [new file with mode: 0644]
src/tlib/jabber/component.py [new file with mode: 0644]
src/tlib/jabber/jid.py [new file with mode: 0644]
src/tlib/jabber/jstrports.py [new file with mode: 0644]
src/tlib/jabber/xmpp_stringprep.py [new file with mode: 0644]
src/tlib/msn.py [new file with mode: 0644]
src/tlib/proxy.py [new file with mode: 0644]
src/tlib/xmlstream.py [new file with mode: 0644]
src/utils.py [new file with mode: 0644]
src/xdb.py [new file with mode: 0644]
src/xmlconfig.py [new file with mode: 0644]

diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..984d777
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,339 @@
+                    GNU GENERAL PUBLIC LICENSE
+                           Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+                          675 Mass Ave, Cambridge, MA 02139, 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
+
+       Appendix: 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., 675 Mass Ave, Cambridge, MA 02139, 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.
diff --git a/PyMSNt b/PyMSNt
new file mode 100755 (executable)
index 0000000..a33c171
--- /dev/null
+++ b/PyMSNt
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+cd `dirname $0`/src
+exec -a PyMSNt python main.py
+cd `dirname $0`
+
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..af64581
--- /dev/null
+++ b/README
@@ -0,0 +1,11 @@
+For the install guide check out the setup guide on
+http://msn-transport.jabberstudio.org
+
+For quickstart, copy config-example.xml to config.xml, change the settings there, and run ./PyMSNt
+
+For translations have a look at lang.py. If you need any help starting a translation feel free to ask.
+
+Coding:
+* To implement a new protocol look in the baseproto directory for what functions must be reimplemented.
+* Look at the MSN files for examples
+
diff --git a/TODO b/TODO
new file mode 100644 (file)
index 0000000..eb1ba77
--- /dev/null
+++ b/TODO
@@ -0,0 +1,35 @@
+For some release:
+* Some kind of improvement to the contact list situation:
+ - Update roster-subsync to support contact removal
+ - Disco the user for roster-subsync support and warn them if it's not there
+* Caches list version number for faster login times - not quite working..
+
+
+For 0.10 - The I Want This Right Now release:
+* File transfer (JEP0096)
+* ACL support
+
+
+For 0.11 - The Admin Friendly release:
+* Optional MD5 hashing for the spool directory
+* Web configuration interface, maybe I should do it with JEP0004 - Data forms?
+
+
+For 0.12 - The big-site friendly release:
+* Clustering
+  - Have msn[0-99].host pointed to by msn.host which tracks sessions to route packets
+
+
+For 0.13:
+* Fix as many bugs as possible for...
+
+1.0:
+All of the above!
+
+
+
+
+Features for after 1.0:
+* Data forms (JEP0004) registration - with more user-specific config options
+* Avatars (JEP0008) - if anybody wants to do the MSN part, I'm happy to do the Jabber bit :)
+
diff --git a/config-example.xml b/config-example.xml
new file mode 100644 (file)
index 0000000..c495fd8
--- /dev/null
@@ -0,0 +1,74 @@
+<pymsnt>
+<!-- This file contains options to be configured by the server administrator. -->
+<!-- Please read through all the options in this file -->
+
+<!-- The JabberID of the transport -->
+<jid>msn</jid>
+
+
+<!-- The location of the spool directory.. if relative, relative to the PyMSNt dir.
+Do not include the jid of the transport -->
+<!-- <spooldir>/path/to/data</spooldir> -->
+
+<!-- The location of the PID file (if relative, relative to the PyMSNt dir) -->
+<pid>PyMSNt.pid</pid>
+
+
+<!-- The IP address of the main Jabber server to connect to -->
+<mainServer>127.0.0.1</mainServer>
+<!-- The JID of the main Jabber server -->
+<mainServerJID>host.com</mainServerJID>
+<!-- The website of the Jabber service -->
+<website>http://host.com</website>
+<!-- The TCP port to connect to the Jabber server on (this is the default for Jabberd2) -->
+<port>5347</port>
+<!-- The authentication token to use when connecting to the Jabber server -->
+<secret>secret</secret>
+
+
+<!-- The default language to use -->
+<lang>en</lang>
+
+
+<!-- Comment out the following options to disable them, or uncomment them to enable them -->
+<!-- Send email notification messages to users -->
+<mailNotifications/>
+<!-- Use fancy friendly names (based on your Jabber status message) -->
+<fancyFriendly/>
+<!-- Send greeting on login -->
+<!-- <sessionGreeting>You have just started a session with PyMSNt</sessionGreeting> -->
+<!-- Send message on successful registration -->
+<!-- <registerMessage>You have successfully registered with PyMSNt</registerMessage> -->
+<!-- Allow users to register with this transport -->
+<allowRegister/>
+
+
+
+<!-- You can select which event loop PyMSNt will use. It's probably safe to leave this as the default -->
+<!-- Use epoll for high-load Linux servers running kernel 2.6 or above -->
+<!--<reactor>epoll</reactor>-->
+
+<!-- Use kqueue for high-load FreeBSD servers -->
+<!--<reactor>kqueue</reactor>-->
+
+<!-- Use poll for high-load Unix servers -->
+<!--<reactor>poll</reactor>-->
+
+
+
+<!-- HTTPS proxy settings. To use a proxy, set both these values -->
+<!--
+<proxyServer>someserver.com</proxyServer>
+<proxyPort>443</proxyPort>
+-->
+
+<!-- Set this to get debugging output -->
+<debugOn/>
+<!-- Set this to only get debugging output when a problem occurs. Only applies if logging to a file -->
+<debugSmart/>
+<!-- Set the debug log file location here, (comment out to output to the screen) -->
+<!-- (if relative, relative to the PyMSNt dir) -->
+<debugLog>debug.log</debugLog>
+
+
+</pymsnt>
diff --git a/src/baseproto/__init__.py b/src/baseproto/__init__.py
new file mode 100644 (file)
index 0000000..df8b9dd
--- /dev/null
@@ -0,0 +1,3 @@
+from glue import LegacyConnection, LegacyGroupchat, translateAccount
+from glue import name, version, mangle, id, namespace
+from glue import formRegEntry, getAttributes, isGroupJID
diff --git a/src/baseproto/glue.py b/src/baseproto/glue.py
new file mode 100644 (file)
index 0000000..d273dfc
--- /dev/null
@@ -0,0 +1,113 @@
+# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Licensed for distribution under the GPL version 2, check COPYING for details
+
+from tlib.domish import Element
+import groupchat
+
+# The name of the transport
+name = "Foo Transport"
+
+# The transport version
+version = "0.1"
+
+# XDB '@' -> '%' mangling
+mangle = True
+
+# The transport identifier (eg, aim, icq, msn)
+id = "foo"
+
+# This should be set to the name space registration entries are in, in the xdb spool
+namespace = "jabber:iq:register"
+
+
+
+def isGroupJID(jid):
+       """ Returns True if the JID passed is a valid groupchat JID (eg, for MSN, if it does not contain '%') """
+       pass
+
+
+
+def formRegEntry(username, password, nickname):
+       """ Returns a domish.Element representation of the data passed. This element will be written to the XDB spool file """
+       pass
+
+
+
+def getAttributes(base):
+       """ This function should, given a spool domish.Element, pull the username, password,
+       and nickname out of it and return them """
+       pass
+#      return username, password, nickname
+
+
+
+
+def translateAccount(legacyaccount):
+       """ Translates the legacy account into a Jabber ID, eg, user@hotmail.com --> user%hotmail.com@msn.jabber.org """
+       pass
+
+
+
+class LegacyGroupchat(groupchat.BaseGroupchat):
+       """ A class to represent a groupchat on the legacy service. All the functions below
+       must be implemented to translate messages from Jabber to the legacy protocol.
+       Look in groupchat.py for available functions to call.
+       """
+       def __init__(self, session, resource, ID=None):
+               groupchat.BaseGroupchat.__init__(self, session, resource, ID)
+               # Initialisation stuff for the legacy protocol goes here
+
+       def removeMe(self):
+               """ Cleanly remove the the groupchat, including removing the user from the legacy room """
+               groupchat.BaseGroupchat.removeMe(self)
+       
+       def sendLegacyMessage(self, message):
+               """ Send this message to the legacy room  """
+       
+       def sendContactInvite(self, contactJID):
+               """ Invite this user to the legacy room """
+
+
+
+
+class LegacyConnection:
+       """ A base class that must have all functions reimplemented by legacy protocols to translate
+       from Jabber to that legacy protocol. Any incoming events from the legacy system must be
+       translated by calling the appropriate functions in the Session, JabberConnection or PyTransport classes.
+       You must also set self.session.ready = True at some point (usually when you have been connected to the
+       legacy service """
+       def __init__(self, session):
+               pass
+       
+       def removeMe(self):
+               """ Called by PyTransport when the user's session is ending.
+               Must cleanly delete this object. Including sending an offline presence packet
+               for any contacts that may be on the user's list """
+               pass
+       
+       def resourceOffline(self, resource):
+               """ Called whenever one of the local user's resources goes offline """
+               pass
+       
+       def sendMessage(self, dest, resource, body, noerror):
+               """ Called whenever PyTransport wants to send a message to a remote user """
+               pass
+       
+       def setStatus(self, show, friendly):
+               """ Called whenever PyTransport needs to change the status on the legacy service 
+               'show' is a Jabber status description, and friendly is a friendly name for the contact """
+               pass
+       
+       def newResourceOnline(self, resource):
+               """ Called by PyTransport when a new resource comes online. You should send them any legacy contacts' status """
+               pass
+       
+       def jabberSubscriptionReceived(self, to, subtype):
+               """ Called by PyTransport whenever a Jabber subscription packet is received """
+               pass
+       
+       def userTypingNotification(self, dest, composing):
+               """ Called by PyTransport whenever the Jabber user has sent typing notification to a contact """
+               pass
+
+
diff --git a/src/config.py b/src/config.py
new file mode 100644 (file)
index 0000000..9090172
--- /dev/null
@@ -0,0 +1,31 @@
+# This file contains the default settings for various options.
+# Please edit config.xml instead of this file
+
+jid = "msn"
+spooldir = ""
+pid = "PyMSNt.pid"
+
+mainServer = "127.0.0.1"
+mainServerJID = ""
+website = ""
+port = "5347"
+secret = "secret"
+
+lang = "en"
+
+mailNotifications = False
+fancyFriendly = False
+sessionGreeting = ""
+registerMessage = ""
+allowRegister = False
+
+reactor = ""
+
+proxyServer = ""
+proxyPort = ""
+
+debugOn = False
+debugSmart = False
+debugLog = ""
+
+
diff --git a/src/debug.py b/src/debug.py
new file mode 100644 (file)
index 0000000..d284ee9
--- /dev/null
@@ -0,0 +1,76 @@
+# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Licensed for distribution under the GPL version 2, check COPYING for details
+
+import os
+import sys
+import config
+import utils
+import time
+
+""" A simple logging module. Use as follows.
+
+> import debug
+> debug.log("text string")
+
+If debugging is enabled then the data will be dumped to a file
+or the screen (whichever the user chose)
+"""
+
+
+file = None
+rollingStack = None
+if(config.debugSmart):
+       rollingStack = utils.RollingStack(100)
+
+
+def reopenFile(first=False):
+       global file
+       if(file or first):
+               if(file): file.close()
+
+               try:
+                       file = open(utils.doPath(config.debugLog), 'a')
+               except:
+                       print "Error opening debug log file. Exiting..."
+                       os.abort()
+
+
+def flushDebugSmart():
+       global rollingStack
+       if(config.debugSmart):
+               file.write(rollingStack.grabAll())
+               rollingStack.flush()
+               file.flush()
+
+
+if(config.debugOn):
+       if(len(config.debugLog) > 0):
+               reopenFile(True)
+               def log(data, wtime=True):
+                       text = ""
+                       if(wtime):
+                               text += time.strftime("%D - %H:%M:%S - ")
+                       text += utils.latin1(data) + "\n"
+                       if(config.debugSmart):
+                               rollingStack.push(text)
+                       else:
+                               file.write(text)
+                               file.flush()
+       else:
+               def log(data, wtime=True):
+                       if(wtime):
+                               print time.strftime("%D - %H:%M:%S - "),
+                       print utils.latin1(data)
+                       sys.stdout.flush()
+       log("Debug logging enabled.")
+else:
+       def log(data):
+               pass
+
+
+def write(data):
+       # So that I can pass this module to twisted.python.failure.Failure.printDetailedTraceback() as a file
+       data = data.rstrip()
+       log(data)
+
+
diff --git a/src/disco.py b/src/disco.py
new file mode 100644 (file)
index 0000000..000677e
--- /dev/null
@@ -0,0 +1,171 @@
+# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Licensed for distribution under the GPL version 2, check COPYING for details
+
+import utils
+if(utils.checkTwisted()):
+       from twisted.xish.domish import Element
+else:
+       from tlib.domish import Element
+from twisted.internet.defer import Deferred
+from twisted.internet import reactor
+import sys
+import config
+import debug
+import legacy
+
+XMPP_STANZAS = 'urn:ietf:params:xml:ns:xmpp-stanzas'
+DISCO = "http://jabber.org/protocol/disco"
+DISCO_ITEMS = DISCO + "#items"
+DISCO_INFO = DISCO + "#info"
+
+
+class ServerDiscovery:
+       def __init__ (self, pytrans):
+               debug.log("Discovery: Created server discovery manager")
+               self.pytrans = pytrans
+               self.identities = []
+               self.features = []
+               self.deferredIqs = {} # A dict indexed by (jid, id) of deferreds to fire
+               
+               self.addFeature(DISCO, None)
+       
+       def sendIq(self, el, timeout=15):
+               """ Used for sending IQ packets.
+               The id attribute for the IQ will be autogenerated if it is not there yet.
+               Returns a deferred which will fire with the matching IQ response as it's sole argument. """
+               def checkDeferred():
+                       if(not d.called):
+                               d.errback()
+                               del self.deferredIqs[(jid, ID)]
+
+               jid = el.getAttribute("to")
+               ID = el.getAttribute("id")
+               if(not ID):
+                       ID = self.pytrans.makeMessageID()
+                       el.attributes["id"] = ID
+               self.pytrans.send(el)
+               d = Deferred()
+               self.deferredIqs[(jid, ID)] = d
+               reactor.callLater(timeout, checkDeferred)
+               return d
+       
+       def addIdentity(self, category, ctype, name):
+               debug.log("Discovery: Adding identitity \"%s\" \"%s\" \"%s\"" % (category, ctype, name))
+               self.identities.append((category, ctype, name))
+       
+       def addFeature(self, var, handler):
+               debug.log("Discovery: Adding feature support \"%s\" \"%s\"" % (var, handler))
+               self.features.append((var, handler))
+       
+       def onIq(self, el):
+               fro = el.getAttribute("from")
+               to = el.getAttribute("to")
+               ID = el.getAttribute("id")
+               iqType = el.getAttribute("type")
+
+               # Check if it's a response to a send IQ
+               if(self.deferredIqs.has_key((fro, ID)) and iqType in ["error", "result"]):
+                       self.deferredIqs[(fro, ID)].callback(el)
+                       del self.deferredIqs[(fro, ID)]
+                       return
+
+               if(iqType not in ["get", "set"]): return # Not interested       
+
+               debug.log("Discovery: Iq received \"%s\" \"%s\". Looking for handler" % (fro, ID))
+
+               for query in el.elements():
+                       xmlns = query.defaultUri
+                       
+                       if(to.find('@') > 0): # Iq to a user
+                               self.sendIqNotSupported(to=fro, fro=config.jid, ID=ID, xmlns=DISCO)
+                       
+                       else: # Iq to transport
+                               if(xmlns == DISCO_INFO):
+                                       self.sendDiscoInfoResponse(to=fro, ID=ID)
+                               elif(xmlns == DISCO_ITEMS):
+                                       self.sendDiscoItemsResponse(to=fro, ID=ID)
+                               else:
+                                       handled = False
+                                       for (feature, handler) in self.features:
+                                               if(feature == xmlns and handler):
+                                                       debug.log("Discovery: Handler found \"%s\" \"%s\"" % (feature, handler))
+                                                       handler(el)
+                                                       handled = True
+                                       if(not handled):
+                                               debug.log("Discovery: Unknown Iq request \"%s\" \"%s\" \"%s\"" % (fro, ID, xmlns))
+                                               self.sendIqNotSupported(to=fro, fro=config.jid, ID=ID, xmlns=DISCO)
+       
+       def sendDiscoInfoResponse(self, to, ID):
+               debug.log("Discovery: Replying to disco#info request from \"%s\" \"%s\"" % (to, ID))
+               iq = Element((None, "iq"))
+               iq.attributes["type"] = "result"
+               iq.attributes["from"] = config.jid
+               iq.attributes["to"] = to
+               if(ID):
+                       iq.attributes["id"] = ID
+               query = iq.addElement("query")
+               query.attributes["xmlns"] = DISCO_INFO
+               
+               # Add any identities
+               for (category, ctype, name) in self.identities:
+                       identity = query.addElement("identity")
+                       identity.attributes["category"] = category
+                       identity.attributes["type"] = ctype
+                       identity.attributes["name"] = name
+               
+               # Add any supported features
+               for (var, handler) in self.features:
+                       feature = query.addElement("feature")
+                       feature.attributes["var"] = var
+               self.pytrans.send(iq)
+       
+       def sendDiscoItemsResponse(self, to, ID):
+               debug.log("Discovery: Replying to disco#items request from \"%s\" \"%s\"" % (to, ID))
+               iq = Element((None, "iq"))
+               iq.attributes["type"] = "result"
+               iq.attributes["from"] = config.jid
+               iq.attributes["to"] = to
+               if(ID):
+                       iq.attributes["id"] = ID
+               query = iq.addElement("query")
+               query.attributes["xmlns"] = DISCO_ITEMS
+               
+               self.pytrans.send(iq)
+       
+       
+       def sendIqNotSupported(self, to, fro, ID, xmlns):
+               debug.log("Discovery: Replying with error to unknown Iq request")
+               iq = Element((None, "iq"))
+               iq.attributes["type"] = "error"
+               iq.attributes["from"] = fro
+               iq.attributes["to"] = to
+               if(ID):
+                       iq.attributes["id"] = ID
+               error = iq.addElement("error")
+               error.attributes["xmlns"] = xmlns
+               error.attributes["type"] = "cancel"
+               error.attributes["xmlns"] = XMPP_STANZAS
+               text = error.addElement("text")
+               text.attributes["xmlns"] = XMPP_STANZAS
+               text.addContent("Not implemented.")
+               
+               self.pytrans.send(iq)
+       
+       def sendIqNotValid(self, to, ID, xmlns):
+               debug.log("Discovery: Replying with error to invalid Iq request")
+               iq = Element((None, "iq"))
+               iq.attributes["type"] = "error"
+               iq.attributes["from"] = config.jid
+               iq.attributes["to"] = to
+               if(ID):
+                       iq.attributes["id"] = ID
+               error = iq.addElement("error")
+               error.attributes["xmlns"] = xmlns
+               error.attributes["type"] = "modify"
+               error.attributes["xmlns"] = XMPP_STANZAS
+               text = error.addElement("text")
+               text.attributes["xmlns"] = XMPP_STANZAS
+               text.addContent("Not valid.")
+               
+               self.pytrans.send(iq)
+
diff --git a/src/groupchat.py b/src/groupchat.py
new file mode 100644 (file)
index 0000000..2eeef61
--- /dev/null
@@ -0,0 +1,160 @@
+# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Licensed for distribution under the GPL version 2, check COPYING for details
+
+import utils
+from twisted.internet import reactor
+if(utils.checkTwisted()):
+       from twisted.xish.domish import Element
+else:
+       from tlib.domish import Element
+import jabw
+import config
+import debug
+import lang
+import string
+import time
+
+
+class BaseGroupchat:
+       """ A class to map a groupchat from a legacy service back to the Jabber user """
+       def __init__(self, session, resource, ID=None):
+               self.session = session
+               self.session.groupchats.append(self)
+               self.nick = resource
+               if(ID):
+                       self.ID = ID
+                       self.session.pytrans.reserveID(self.ID)
+               else:
+                       self.ID = self.session.pytrans.makeID()
+               
+               self.ready = False # Is True only after the user has joined
+               self.messageBuffer = []
+               self.contacts = []
+               
+               self.checkTimer = reactor.callLater(60.0*2, self.checkUserJoined, None)
+               
+               debug.log("BaseGroupchat: \"%s\" created" % (self.roomJID()))
+       
+       def removeMe(self):
+               """ Cleanly removes the object """
+               self.session.groupchats.remove(self)
+               if(self.ready):
+                       self.session.sendPresence(to=self.user(), fro=self.roomJID() + "/" + self.nick, ptype="unavailable")
+               self.ready = False
+               self.session = None
+               
+               if(self.checkTimer and not self.checkTimer.called):
+                       self.checkTimer.cancel()
+               self.checkTimer = None
+
+               utils.mutilateMe(self)
+               
+               debug.log("BaseGroupchat: \"%s\" destroyed" % (self.roomJID()))
+       
+       def roomJID(self):
+               """ Returns the room JID """
+               return self.ID + "@" + config.jid
+       
+       def user(self):
+               """ Returns the full JID of the Jabber user in this groupchat """
+               jid = self.session.jabberID
+               # FIXME, this probably won't work with multiple resources (unless you're using the highest resource)
+#              if(self.resource):
+#                      jid += "/" + self.resource
+               return jid
+       
+       def checkUserJoined(self, ignored=None):
+               self.checkTimer = None
+               if(not self.ready):
+                       debug.log("BaseGroupchat: \"%s\" User hasn't joined after two minutes. Removing them from the room.")
+                       
+                       text = []
+                       text.append(lang.get(self.session.lang).groupchatFailJoin1 % (self.roomJID()))
+                       for contact in self.contacts:
+                               text.append("\t%s" % (contact))
+                       text.append("")
+                       text.append(lang.get(self.session.lang).groupchatFailJoin2)
+                       text.append("")
+                       for (source, message, timestamp) in self.messageBuffer:
+                               if(source):
+                                       text.append("%s says: %s" % (source, message))
+                               else:
+                                       text.append(message)
+                       
+                       body = string.join(text, "\n")
+                       
+                       self.session.sendMessage(to=self.user(), fro=config.jid, body=body)
+                       
+                       self.removeMe()
+       
+       def sendUserInvite(self, fro):
+               """ Sends the invitation out to the Jabber user to join this room """
+               el = Element((None, "message"))
+               el.attributes["from"] = fro
+               el.attributes["to"] = self.user()
+               body = el.addElement("body")
+               text = lang.get(self.session.lang).groupchatInvite % (self.roomJID())
+               body.addContent(text)
+               x = el.addElement("x")
+               x.attributes["jid"] = self.roomJID()
+               x.attributes["xmlns"] = "jabber:x:conference"
+               debug.log("BaseGroupchat: \"%s\" sending invitation to \"%s\" to join" % (self.roomJID(), self.user()))
+               self.session.pytrans.send(el)
+       
+       def userJoined(self, nick):
+               # Send any buffered messages
+               self.nick = nick
+               if(not self.nick):
+                       self.nick = self.session.username
+               self.session.sendPresence(to=self.user(), fro=self.roomJID() + "/" + self.nick)
+               if(not self.ready):
+                       debug.log("BaseGroupchat: \"%s\" user has joined us!" % (self.roomJID()))
+                       self.ready = True
+                       for (source, text, timestamp) in self.messageBuffer:
+                               self.messageReceived(source, text, timestamp)
+                       self.messageBuffer = None
+                       for contact in self.contacts:
+                               self.contactPresenceChanged(contact)
+       
+       def contactJoined(self, contact):
+               if(self.contacts.count(contact) == 0):
+                       self.contacts.append(contact)
+                       debug.log("BaseGroupchat: \"%s\" Legacy contact has joined \"%s\"" % (self.roomJID(), contact))
+               self.contactPresenceChanged(contact)
+               self.messageReceived(None, "%s has joined the conference." % (contact))
+       
+       def contactLeft(self, contact):
+               if(self.contacts.count(contact) > 0):
+                       self.contacts.remove(contact)
+                       debug.log("BaseGroupchat: \"%s\" Legacy contact has left \"%s\"" % (self.roomJID(), contact))
+               self.contactPresenceChanged(contact, ptype="unavailable")
+               self.messageReceived(None, "%s has left the conference." % (contact))
+       
+       def messageReceived(self, source, message, timestamp=None):
+               if(not self.ready):
+                       timestamp = time.strftime("%Y%m%dT%H:%M:%S")
+                       self.messageBuffer.append((source, message, timestamp))
+               else:
+                       fro = self.roomJID()
+                       if(source):
+                               fro += "/" + source
+                       debug.log("BaseGroupchat: \"%s\" messageReceived(\"%s\", \"%s\", \"%s\")" % (self.roomJID(), source, message, timestamp))
+                       self.session.sendMessage(to=self.user(), fro=fro, body=message, mtype="groupchat", delay=timestamp)
+       
+       def contactPresenceChanged(self, contact, ptype=None):
+               if(self.session):
+                       fro = self.roomJID() + "/" + contact
+                       self.session.sendPresence(to=self.user(), fro=fro, ptype=ptype)
+       
+       def sendMessage(self, text, noerror):
+               debug.log("BaseGroupchat: \"%s\" sendMessage(\"%s\")" % (self.roomJID(), text))
+               self.messageReceived(self.nick, text)
+               self.sendLegacyMessage(text, noerror)
+       
+       def sendLegacyMessage(self, text):
+               """ Reimplement this to send the packet to the legacy service """
+               pass
+       
+       def sendContactInvite(self, contact):
+               """ Reimplement this to send the packet to the legacy service """
+               pass
diff --git a/src/jabw.py b/src/jabw.py
new file mode 100644 (file)
index 0000000..fa7f4f4
--- /dev/null
@@ -0,0 +1,259 @@
+# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Licensed for distribution under the GPL version 2, check COPYING for details
+
+import utils
+if(utils.checkTwisted()):
+       from twisted.xish.domish import Element
+       from twisted.words.protocols.jabber import jid
+else:
+       from tlib.domish import Element
+       from tlib.jabber import jid
+import debug
+
+
+def sendMessage(pytrans, to, fro, body, mtype=None, delay=None):
+       """ Sends a Jabber message """
+       debug.log("jabw: Sending a Jabber message \"%s\" \"%s\" \"%s\" \"%s\"" % (to, fro, utils.latin1(body), mtype))
+       el = Element((None, "message"))
+       el.attributes["to"] = to
+       el.attributes["from"] = fro
+       el.attributes["id"] = pytrans.makeMessageID()
+       if(mtype):
+               el.attributes["type"] = mtype
+       
+       if(delay):
+               x = el.addElement("x")
+               x.attributes["xmlns"] = "jabber:x:delay"
+               x.attributes["from"] = fro
+               x.attributes["stamp"] = delay
+       
+       b = el.addElement("body")
+       b.addContent(body)
+       x = el.addElement("x")
+       x.attributes["xmlns"] = "jabber:x:event"
+       composing = x.addElement("composing")
+       pytrans.send(el)
+
+def sendPresence(pytrans, to, fro, show=None, status=None, priority=None, ptype=None):
+       # Strip the resource off any presence subscribes (as per XMPP RFC 3921 Section 5.1.6)
+       # Makes eJabberd behave :)
+       if(ptype == "subscribe"):
+               (user,host,res) = jid.parse(to)
+               to = "%s@%s" % (user, host)
+       
+       el = Element((None, "presence"))
+       el.attributes["to"] = to
+       el.attributes["from"] = fro
+       if(ptype):
+               el.attributes["type"] = ptype
+       if(show):
+               s = el.addElement("show")
+               s.addContent(show)
+       if(status):
+               s = el.addElement("status")
+               s.addContent(status)
+       if(priority):
+               s = el.addElement("priority")
+               s.addContent(priority)
+       pytrans.send(el)
+
+
+def sendErrorMessage(pytrans, to, fro, etype, condition, explanation, body=None):
+       el = Element((None, "message"))
+       el.attributes["to"] = to
+       el.attributes["from"] = fro
+       el.attributes["type"] = "error"
+       error = el.addElement("error")
+       error.attributes["type"] = etype
+       error.attributes["code"] = str(utils.errorCodeMap[condition])
+       desc = error.addElement(condition)
+       desc.attributes["xmlns"] = "urn:ietf:params:xml:ns:xmpp-stanzas"
+       text = error.addElement("text")
+       text.attributes["xmlns"] = "urn:ietf:params:xml:ns:xmpp-stanzas"
+       text.addContent(explanation)
+       if(body and len(body) > 0):
+               b = el.addElement("body")
+               b.addContent(body)
+       pytrans.send(el)
+
+
+
+
+class JabberConnection:
+       """ A class to handle a Jabber "Connection", ie, the Jabber side of the gateway.
+       If you want to send a Jabber event, this is the place, and this is where incoming
+       Jabber events for a session come to. """
+       
+       def __init__(self, pytrans, jabberID):
+               self.pytrans = pytrans
+               self.jabberID = jabberID
+
+               self.typingUser = False # Whether this user can accept typing notifications
+               self.messageIDs = dict() # The ID of the last message the user sent to a particular contact. Indexed by contact JID
+               
+               debug.log("User: %s - JabberConnection constructed" % (self.jabberID))
+       
+       def removeMe(self):
+               """ Cleanly deletes the object """
+               debug.log("User: %s - JabberConnection removed" % (self.jabberID))
+       
+       def checkFrom(self, el):
+               """ Checks to see that this packet was intended for this object """
+               fro = el.getAttribute("from")
+               froj = jid.JID(fro)
+               
+               return (froj.userhost() == self.jabberID) # Compare with the Jabber ID that we're looking at
+       
+       def sendMessage(self, to, fro, body, mtype=None, delay=None):
+               """ Sends a Jabber message 
+               For this message to have a <x xmlns="jabber:x:delay"/> you must pass a correctly formatted timestamp (See JEP0091)
+               """
+               debug.log("User: %s - JabberConnection sending message \"%s\" \"%s\" \"%s\" \"%s\"" % (self.jabberID, to, fro, utils.latin1(body), mtype))
+               sendMessage(self.pytrans, to, fro, body, mtype, delay)
+       
+       def sendTypingNotification(self, to, fro, typing):
+               """ Sends the user the contact's current typing notification status """
+               if(self.typingUser):
+                       debug.log("jabw: Sending a Jabber typing notification message \"%s\" \"%s\" \"%s\"" % (to, fro, typing))
+                       el = Element((None, "message"))
+                       el.attributes["to"] = to
+                       el.attributes["from"] = fro
+                       x = el.addElement("x")
+                       x.attributes["xmlns"] = "jabber:x:event"
+                       if(typing):
+                               composing = x.addElement("composing") 
+                       id = x.addElement("id")
+                       if(self.messageIDs.has_key(fro) and self.messageIDs[fro]):
+                               id.addContent(self.messageIDs[fro])
+                       self.pytrans.send(el)
+       
+       def sendErrorMessage(self, to, fro, etype, condition, explanation, body=None):
+               debug.log("User: %s - JabberConnection sending error response." % (self.jabberID))
+               sendErrorMessage(self.pytrans, to, fro, etype, condition, explanation, body)
+       
+       def sendPresence(self, to, fro, show=None, status=None, priority=None, ptype=None):
+               """ Sends a Jabber presence packet """
+               debug.log("User: %s - JabberConnection sending presence \"%s\" \"%s\" \"%s\" \"%s\" \"%s\" \"%s\"" % (self.jabberID, to, fro, show, utils.latin1(status), priority, ptype))
+               sendPresence(self.pytrans, to, fro, show, status, priority, ptype)
+       
+       def sendRosterImport(self, jid, ptype, sub, name="", groups=[]):
+               """ Sends a special presence packet. This will work with all clients, but clients that support roster-import will give a better user experience
+               IMPORTANT - Only ever use this for contacts that have already been authorised on the legacy service """
+               el = Element((None, "presence"))
+               el.attributes["to"] = self.jabberID
+               el.attributes["from"] = jid
+               el.attributes["type"] = ptype
+               r = el.addElement("x")
+               r.attributes["xmlns"] = "http://jabber.org/protocol/roster-subsync"
+               item = r.addElement("item")
+               item.attributes["subscription"] = sub
+               if(name):
+                       item.attributes["name"] = unicode(name)
+               for group in groups:
+                       g = item.addElement("group")
+                       g.addContent(group)
+               
+               self.pytrans.send(el)
+       
+       def onMessage(self, el):
+               """ Handles incoming message packets """
+               if(not self.checkFrom(el)): return
+               debug.log("User: %s - JabberConnection received message packet" % (self.jabberID))
+               fro = el.getAttribute("from")
+               froj = jid.JID(fro)
+               to = el.getAttribute("to")
+               toj = jid.JID(to)
+               mID = el.getAttribute("id")
+               
+               mtype = el.getAttribute("type")
+               body = ""
+               invite = ""
+               messageEvent = False
+               noerror = False
+               composing = None
+               for child in el.elements():
+                       if(child.name == "body"):
+                               body = child.__str__()
+                       if(child.name == "noerror" and child.uri == "sapo:noerror"):
+                               noerror = True
+                       if(child.name == "x"):
+                               if(child.uri == "jabber:x:conference"):
+                                       invite = child.getAttribute("jid") # The room the contact is being invited to
+                               if(child.uri == "jabber:x:event"):
+                                       messageEvent = True
+                                       composing = False
+                                       for deepchild in child.elements():
+                                               if(deepchild.name == "composing"):
+                                                       composing = True
+               
+               if(invite):
+                       debug.log("User: %s - JabberConnection parsed message groupchat invite packet \"%s\" \"%s\" \"%s\" \"%s\"" % (self.jabberID, froj.userhost(), to, froj.resource, utils.latin1(invite)))
+                       self.inviteReceived(froj.userhost(), froj.resource, toj.userhost(), toj.resource, invite)
+                       return
+
+               # Check message event stuff
+               if(body and messageEvent):
+                       self.typingUser = True
+               elif(body and not messageEvent):
+                       self.typingUser = False
+               elif(not body and messageEvent):
+                       debug.log("User: %s - JabberConnection parsed typing notification \"%s\" \"%s\"" % (self.jabberID, toj.userhost(), composing))
+                       self.typingNotificationReceived(toj.userhost(), toj.resource, composing)
+                       
+                       
+               if(body):
+#                      body = utils.utf8(body)
+                       # Save the message ID for later
+                       self.messageIDs[to] = mID
+                       debug.log("User: %s - JabberConnection parsed message packet \"%s\" \"%s\" \"%s\" \"%s\" \"%s\"" % (self.jabberID, froj.userhost(), to, froj.resource, mtype, utils.latin1(body)))
+                       self.messageReceived(froj.userhost(), froj.resource, toj.userhost(), toj.resource, mtype, body, noerror)
+       
+       def onPresence(self, el):
+               """ Handles incoming presence packets """
+               if(not self.checkFrom(el)): return
+               debug.log("User: %s - JabberConnection received presence packet" % (self.jabberID))
+               fro = el.getAttribute("from")
+               froj = jid.JID(fro)
+               to = el.getAttribute("to")
+               toj = jid.JID(to)
+               
+               # Grab the contents of the <presence/> packet
+               ptype = el.getAttribute("type")
+               if(ptype in ["subscribe", "subscribed", "unsubscribe", "unsubscribed"]):
+                       debug.log("User: %s - JabberConnection parsed subscription presence packet \"%s\" \"%s\"" % (self.jabberID, toj.userhost(), ptype))
+                       self.subscriptionReceived(toj.userhost(), ptype)
+               else:
+                       status = None
+                       show = None
+                       priority = None
+                       for child in el.elements():
+                               if(child.name == "status"):
+                                       status = child.__str__()
+                               elif(child.name == "show"):
+                                       show = child.__str__()
+                               elif(child.name == "priority"):
+                                       priority = child.__str__()
+                       
+                       debug.log("User: %s - JabberConnection parsed presence packet \"%s\" \"%s\" \"%s\" \"%s\" \"%s\" \"%s\"" % (self.jabberID, froj.userhost(), froj.resource, priority, ptype, show, utils.latin1(status)))
+                       self.presenceReceived(froj.userhost(), froj.resource, toj.userhost(), toj.resource, priority, ptype, show, status)
+       
+       
+       
+       def messageReceived(self, source, resource, dest, destr, mtype, body, noerror):
+               """ Override this method to be notified when a message is received """
+               pass
+       
+       def inviteReceived(self, source, resource, dest, destr, roomjid):
+               """ Override this method to be notified when an invitation is received """
+               pass
+       
+       def presenceReceived(self, source, resource, to, tor, priority, ptype, show, status):
+               """ Override this method to be notified when presence is received """
+               pass
+       
+       def subscriptionReceived(self, source, subtype):
+               """ Override this method to be notified when a subscription packet is received """
+               pass
+
+
+
diff --git a/src/lang.py b/src/lang.py
new file mode 100644 (file)
index 0000000..ecc7327
--- /dev/null
@@ -0,0 +1,183 @@
+# -*- coding: UTF-8 -*-
+
+import config
+
+def get(lang=config.lang):
+       if(not lang.__class__ in [str, unicode]):
+               lang = config.lang
+       try:
+               lang = lang.replace("-", "_")
+               return strings.__dict__[lang]
+       except KeyError:
+               return strings.__dict__[config.lang]
+
+
+# If you change or add any strings in this file please contact the translators listed below
+# Everything must be in UTF-8
+# Look for language codes here - http://www.w3.org/WAI/ER/IG/ert/iso639.htm
+# Current languages: English, Portugese, Dutch, German, French, Spanish
+
+class strings:
+       class en: # English - James Bunton <mailto:james@delx.cjb.net>
+               # Text that may get sent to the user. Useful for translations. Keep any %s symbols you see or you will have troubles later
+               registerText = u"Please type your MSN Passport (user@hotmail.com) into the username field, your password and desired base nickname.\nFor more information see http://msn-transport.jabberstudio.org/docs/users"
+               gatewayTranslator = u"Enter the user's MSN account."
+               userMapping = u"The MSN contact %s has a Jabber ID %s. It is recommended to talk to this person through Jabber."
+               notLoggedIn = u"Error. You must log into the transport before sending messages."
+               notRegistered = u"Sorry. You do not appear to be registered with this transport. Please register and try again. If you are having trouble registering please contact your Jabber administrator."
+               waitForLogin = u"Sorry, this message cannot be delivered yet. Please try again when the transport has finished logging in."
+               groupchatInvite = u"You have been invited into a groupchat on the legacy service. You must join this room to switch into groupchat mode %s.\nIf you do not join this room you will not be able to participate in the groupchat, but you will still appear to have joined it to contacts on the MSN service."
+               groupchatFailJoin1 = u"You did not join the groupchat room %s.\nThe following users were in the groupchat:"
+               groupchatFailJoin2 = u"You have been removed from this room on the legacy service. The following was said before you were disconnected, while you appeared to be in the groupchat to the contacts on the legacy service."
+               groupchatPrivateError = u"Sorry. You cannot send private messages to users in this groupchat. Please instead add the user to your contact list and message them that way."
+               groupchatAdvocacy = u"%s has invited you to a Jabber chatroom. To join this room you need to be using Jabber. Please see %s for more information."
+               msnMaintenance = u"Notification from Microsoft. The MSN Messenger network will be going down for maintenance."
+               msnMultipleLogin = u"Your MSN account has been logged in elsewhere. Please logout at the other location and then reactivate the MSN transport."
+               msnNotVerified = u"Your MSN passport %s, has not had it's email address verified. MSN users will not be able to see your nickname, and will be warned that your account may not be legitimate. Please see Microsoft for details."
+               msnLoginFailure = u"MSN transport could not log into your MSN account %s. Please check that your password is correct. You may need to re-register the transport."
+               msnFailedMessage = u"This message could not be delivered. Please check that the contact is online, and that their address on your contact list is correct.\n\n"
+               msnInitialMail = u"Hotmail notification\n\nUnread message in inbox: %s\nUnread messages in folders: %s"
+               msnRealtimeMail = u"Hotmail notification\n\nFrom: %s <%s>\n Subject: %s"
+               msnDisconnected = u"Disconnection from MSN servers: %s"
+       en_US = en # en-US is the same as en, so are the others
+       en_AU = en
+       en_GB = en
+
+       class pt: # asantos
+               # Text that may get sent to the user. Useful for translations. Keep any %s symbols you see or you will have troubles later
+               registerText = u"Para acederes ao Serviço de MSN, tens que inserir o teu username (eg. alex@hotmail.com) e password respectiva."
+               gatewayTranslator = u"Enter the user's MSN account."
+               userMapping = u"O contacto MSN %s tem o seguinte Jabber ID %s. Ã‰ recomendável falar com este contacto através do Jabber."
+               notLoggedIn = u"Erro. Tens que efectuar o login no serviço de transporte antes de começar a enviar mensagens."
+               notRegistered = u"Pedimos Desculpa mas não deverás ter o registo correcto neste serviço de transporte. Tenta registar-te novamente, por favor. Se continuas a ter problemas no registo, contacta-nos por favor (eg. messenger@hotmail.com)."
+               waitForLogin = u"Erro, esta mensagem não poderá ser entregue imediatamente. Por favor tenta de novo, quando o serviço de transporte acabar de efectuar login."
+               groupchatInvite = u"You have been invited into a groupchat on the legacy service. You must join this room to switch into groupchat mode %s.\nIf you do not join this room you will not be able to participate in the groupchat, but you will still appear to have joined it to contacts on the MSN service."
+               groupchatFailJoin1 = u"You did not join the groupchat room %s.\nThe following users were in the groupchat:"
+               groupchatFailJoin2 = u"You have been removed from this room on the legacy service. The following was said before you were disconnected, while you appeared to be in the groupchat to the contacts on the legacy service."
+               groupchatPrivateError = u"Sorry. You cannot send private messages to users in this groupchat. Please instead add the user to your contact list and message them that way."
+               groupchatAdvocacy = u"%s has invited you to a Jabber chatroom. To join this room you need to be using Jabber. Please see %s for more information."
+               msnMaintenance = u"Notification from Microsoft. The MSN Messenger network will be going down for maintenance."
+               msnMultipleLogin = u"A tua conta de MSN foi activada noutro computador. Por favor desliga a ligação no outro computador para retomar o serviço de transporte."
+               msnNotVerified = u"O teu MSN passport %s, não verificou correctamente o teu email. Utilizadores de MSN não vão conseguir ver o teu nickname, e vão ser avisados que a tua conta poderá não ser legitima. Confirma com a Microsoft os teus detalhes."
+               msnLoginFailure = u"O serviço de transporte de MSN não conseguiu activar a ligação com a tua conta %s. Confirma se a tua password está correcta. Poderás ter que te registar de novo no serviço."
+               msnFailedMessage = u"Esta mensagem não pode ser entregue. Confirma por favor se o contacto está online e se o endereço usado na buddylist está correcto\n\n"
+               msnInitialMail = u"Hotmail notification\n\nUnread message in inbox: %s\nUnread messages in folders: %s"
+               msnRealtimeMail = u"Hotmail notification\n\nFrom: %s <%s>\n Subject: %s"
+               msnDisconnected = u"Desligado dos servidores MSN: %s"
+
+       class nl: # Dutch - Matthias Therry <matthias.therry@pi.be>
+               registerText = u"Voer uw MSN Passport (gebruiker@hotmail.com) en uw wachtwoord in. Geef ook het vaste deel van uw bijnaam op.\nRaadpleeg voor meer informatie http://msn-transport.jabberstudio.org/docs/user"
+               gatewayTranslator = u"Voer de MSN-account van de gebruiker in."
+               userMapping = u"Contactpersoon %s op het MSN-netwerk heeft ook een Jabber-ID. Het is het best om met hem via Jabber te chatten. Zijn Jabber-ID is %s."
+               notLoggedIn = u"Fout: u moet eerst aanmelden op het transport alvorens berichten te verzenden."
+               notRegistered = u"Sorry, maar u bent niet geregistreerd op dit transport. Registreer u eerst en probeer daarna opnieuw. Contacteer de beheerder van uw Jabber-server bij registratieproblemen."
+               waitForLogin = u"Sorry, maar dit bericht kon nog niet worden afgeleverd. Probeer opnieuw wanneer het transport klaar is met aanmelden."
+               groupchatInvite = u"U bent uitgenodigd voor een groepsgesprek op het MSN-netwerk. Neem deel door om te schakelen naar groepsgesprekmodus %s.\nAls u dit niet doet, zal u niet kunnen deelnemen aan het gesprek terwijl het voor de MSN-gebruikers lijkt alsof u toch aanwezig bent."
+               groupchatFailJoin1 = u"U hebt niet deelgenomen aan het groepsgesprek in de chatruimte %s.\nVolgende personen waren er aanwezig:"
+               groupchatFailJoin2 = u"U werd verwijderd uit deze chatruimte op het MSN-netwerk. Terwijl u voor de andere deelnemers in deze ruimte aanwezig leek, werd het volgende gezegd:"
+               groupchatPrivateError = u"Sorry, maar u kunt geen privé-berichten verzenden naar gebruikers in deze chatruimte. Voeg de gebruiker daarom toe aan uw contactpersonenlijst van MSN om hem zo persoonlijk te kunnen benaderen."
+               groupchatAdvocacy = u"%s heeft u uitgenodigd op een chatruimte op het Jabber-netwerk. Deze ruimte kunt u alleen betreden via Jabber-netwerk. Neem een kijkje op %s voor meer informatie."
+               msnMaintenance = u"Bericht van Microsoft: het MSN-netwerk zal tijdelijk niet bereikbaar zijn door onderhoudswerken."
+               msnMultipleLogin = u"Uw MSN-account is al ergens anders in gebruik. Meld u daar eerst af en heractiveer vervolgens dit transport."
+               msnNotVerified = u"Het e-mailadres van uw MSN Passport %s werd nog niet geverifieerd. Daardoor zien MSN-gebruikers uw bijnaam niet en zullen ze gewaarschuwd worden dat uw account mogelijk nep is. Contacteer Microsoft voor meer informatie."
+               msnLoginFailure = u"Het MSN-transport kon niet aanmelden op uw MSN-account %s. Controleer uw wachtwoord. Mogelijk moet u zich opnieuw registreren op dit transport."
+               msnFailedMessage = u"Dit bericht kon niet worden afgeleverd. Controleer of de contactpersoon online is en of zijn adres op uw contactpersonenlijst juist is.\n\n"
+               msnInitialMail = u"Hotmail-meldingen\n\nAantal ongelezen berichten in postvak in: %s\nAantal ongelezen berichten in mappen: %s"
+               msnRealtimeMail = u"Hotmail-meldingen\n\nVan: %s <%s>\n Onderwerp: %s"
+               msnDisconnected = u"De verbinding met de MSN-servers werd verbroken: %s"
+       dut = nl
+       nla = nl
+       
+       
+       
+       class de: # German - Florian Holzhauer <xmpp:fh@jabber.ccc.de>
+               registerText = u"Bitte trage Deine MSN-Passport-ID (user@hotmail.com) als User ein, sowie Dein Passwort und Deinen Nickname.\n Mehr Informationen zu diesem Gateway findest Du unter http://msn-transport.jabberstudio.org/docs/users"
+               gatewayTranslator = u"Enter the user's MSN account." # FIXMELANG!!
+               userMapping = u"The MSN contact %s has a Jabber ID %s. It is recommended to talk to this person through Jabber."
+               notLoggedIn = u"Fehler. Du musst beim Gateway eingeloggt sein bevor Du Nachrichten schicken kannst."
+               notRegistered = u"Sorry, Du scheinst Dich bei diesem Gateway noch nicht registriert zu haben. Bitte registriere Dich und versuche es noch einmal. Bei Problemen wendest Du Dich am besten an Deinen Jabber-Administrator"
+               waitForLogin = u"Sorry, die Nachricht kann noch nicht uebermittelt werden. Bitte versuche es noch einmal wenn das Gateway bei MSN eingeloggt ist."
+               groupchatInvite = u"Du wurdest zu einem MSN-Groupchat eingeladen. Du musst dem Groupchat %s beitreten um teilnehmen zu können, ansonsten kannst Du an dem Groupchat nicht teilnehmen, obwohl es für die anderen Teilnehmer so aussieht, als ob Du im Raum bist."
+               groupchatFailJoin1 = u"Du hast den Groupchat-Raum %s nicht betreten.\n Die folgenden User waren im Groupchat:"
+               groupchatFailJoin2 = u"Du bist aus diesem Groupchat-Raum ausgeloggt worden. Das folgende wurde in dem Chat gesagt bevor du ausgeloggt wurdest:"
+               groupchatPrivateError = u"Du kannst keine privaten Nachrichten an Mitglieder dieses Groupchats schicken. Bitte füge sie stattdessen deinem Roster hinzu."
+               groupchatAdvocacy = u"%s has invited you to a Jabber chatroom. To join this room you need to be using Jabber. Please see %s for more information."
+               msnMaintenance = u"Das MSN Messenger Network wird aus Wartungsgründen von Microsoft heruntergefahren. Bis später!"
+               msnMultipleLogin = u"Du bist bereits mit einem anderen Client im MSN Network eingeloggt. Bitte logge den anderen Client aus und aktiviere dann diesen Transport wieder."
+               msnNotVerified = u"Dein MSN-Account %s hat keine von Microsoft Ã¼berprüfte eMail-Adresse. Andere MSN-User können daher Deinen Nickname nicht sehen und werden gewarnt dass dein Account gefälscht sein koennte. Bitte besuche die MSN-Seiten für Details."
+               msnLoginFailure = u"Der Login beim MSN-Account %s ist fehlgeschlagen. Bitte Ã¼berprüfe Dein Passwort und registriere Dich gegebenenfalls erneut."
+               msnFailedMesage = u"Die Nachricht konnte nicht Ã¼bermittelt werden. Bitte prüfe, dass der Contact online ist, und seine Adresse in deiner Contact­List korrekt ist.\nDie Nachricht war:\n\n"
+               msnInitialMail = u"Hotmail notification\n\nUngelesene Nachrichten in der Inbox: %s\nUngelesene Nachrichten in anderen Ordnern: %s"
+               msnRealtimeMail = u"Hotmail notification\n\nNeue Nachricht von %s <%s>\n Subject: %s"
+               msnDisconnected = u"Die Verbindung zum MSN-Server wurde getrennt: %s"
+
+
+       class fr: # French - Lucas Nussbaum <lucas@lucas-nussbaum.net>
+               # Former translator: Alexandre Viard <mailto:ebola@courrier.homelinux.org>
+               registerText = u"Merci d'entrer votre adresse MSN (utilisateur@hotmail.com) dans le champ utilisateur, votre mot de passe et le pseudonyme désiré.\nPour plus d'informations : http://msn-transport.jabberstudio.org/docs/users"
+               gatewayTranslator = u"Saisir l'adresse MSN du contact."
+               userMapping = u"Votre contact MSN %s a une adresse Jabber %s. Il est recommandé de lui parler par l'intermédiaire de Jabber."
+               notLoggedIn = u"Erreur. Vous devez vous connecter au transport avant d'envoyer un message."
+               notRegistered = u"Désolé. Vous ne semblez pas Ãªtre enregistré auprès du transport. Merci de vous enregistrer et de réessayer. Si vous avez des problèmes pour l'enregistrement, merci de contacter votre administrateur Jabber."
+               waitForLogin = u"Désolé, ce message ne peut pas Ãªtre envoyé maintenant. Merci de réessayer quand le transport aura fini de vous connecter."
+               groupchatInvite = u"Vous avez Ã©té invité Ã  une conférence multi-utilisateurs. Vous devez rejoindre la salle %s pour faire partie de la conférence.\nSi vous ne le faites pas, vous ne pourrez pas y participer, mais vous apparaitrez quand même connecté Ã  la salle pour les autres participants."
+               groupchatFailJoin1 = u"Vous n'avez pas rejoint la salle de conférence %s.\nLes utilisateurs suivant y Ã©taient connectés :"
+               groupchatFailJoin2 = u"Vous avez Ã©té déconnecté de la salle par le serveur MSN. Les messages suivants on Ã©té Ã©changés avant que vous soyez déconnecté, pendant que vous apparaissiez connecté Ã  la salle pour les autres utilisateurs."
+               groupchatPrivateError = u"Désolé. Vous ne pouvez pas envoyer des messages privés aux contacts de cette conférence. Merci de les ajouter Ã  votre liste de contact et leur envoyer un message."
+               groupchatAdvocacy = u"%s vous a invité Ã  une salle de discussion Jabber. Pour rejoindre cette salle, vous devez utiliser Jabber. Jetez un coup d'oeil Ã  %s pour plus d'informations."
+               msnMaintenance = u"Notification en provenance de Microsoft. Le réseau MSN Messenger va Ãªtre arrêté pour maintenance."
+               msnMultipleLogin = u"Votre compte MSN a Ã©té utilisé Ã  un autre endroit. Merci de vous déconnecter de celui-ci et de réactiver le transport MSN."
+               msnNotVerified = u"L'adresse email de votre compte MSN %s n'a pas Ã©té vérifiée. Les utilisateurs MSN ne pourront pas voir votre pseudo et seront informés que votre compte n'est peut-être pas légitime. Merci de vous informer auprès de Microsoft pour plus de détails."
+               msnLoginFailure = u"Le transport MSN n'a pas pu se connecter Ã  votre compte MSN %s. Merci de vérifier que votre mot de passe est correct. Vous aurez peut-être Ã  vous ré-enregistrer avec le transport."
+               msnFailedMessage = u"Ce message n'a pas pu Ãªtre délivré. Merci de vérifier que votre contact est en ligne et que son adresse est correcte.\n\n"
+               msnInitialMail = u"Notification Hotmail\n\n Message(s) non lu(s) dans votre boîte de réception : %s\nMessage(s) non lu(s) dans le dossier : %s"
+               msnRealtimeMail = u"Notification Hotmail\n\nDe: %s <%s>\n Sujet: %s"
+               msnDisconnected = u"Déconnecté du serveur MSN: %s"
+       fr_FR = fr
+       fr_LU = fr
+       fr_CH = fr
+       fr_CA = fr
+       fr_BE = fr
+
+       class es: # Spanish - luis peralta <mailto:peralta@spisa.act.uji.es>
+               # Text that may get sent to the user. Useful for translations. Keep any %s symbols you see or you will have troubles later
+               registerText = u"Por favor, introduce tu cuenta MSN Passport (user@hotmail.com) en el campo de usuario, la contraseña y el nick o apodo que desees.\nPara más información visita http://msn-transport.jabberstudio.org/docs/users"
+               gatewayTranslator = u"Introduce la cuenta de usuario MSN."
+               userMapping = u"El contacto MSN %s tiene Jabber ID %s. Se recomienda que te comuniques con esta persona utilizando Jabber."
+               notLoggedIn = u"Error. Tienes que iniciar sesión en el transporte antes de poder enviar mensajes."
+               notRegistered = u"Lo sentimos. Parece que no estás registrado con este transporte. Por favor, regístrate y prueba de nuevo. Si tienes algún problema registrándote contacta por favor con el administrador del servidor Jabber."
+               waitForLogin = u"Lo sentimos, tu mensaje no puede ser enviado todavía. Vuelve a probar cuando el transporte haya acabado de iniciar sesión."
+               groupchatInvite = u"Te han invitado a una sala de charla a través de MSN. Tienes que entrar a la sala %s para pasar a modo de charla entre varios.\nSi no entras en la sala, no podrás participar, pero a los contactos MSN les parecerá que te has unido a la charla."
+               groupchatFailJoin1 = u"No has entrado en la sala %s.\nLos siguientes usuarios estaban en la sala:"
+               groupchatFailJoin2 = u"Has sido eliminado de la charla a tres en el servicio MSN. Lo siguiente se dijo antes de que salieses, mientras al resto de contactos les parecía que todavía estabas en la sala."
+               groupchatPrivateError = u"Lo sentimos. No puedes mandar mensajes privados a usuarios en esta sala de charla. Por favor, añade al usuario a tu lista de contactos y charla desde ahí."
+               groupchatAdvocacy = u"%s has invited you to a Jabber chatroom. To join this room you need to be using Jabber. Please see %s for more information."
+               msnMaintenance = u"Notificación de Microsoft. La red de MSN Messenger va a estar en mantenimiento."
+               msnMultipleLogin = u"Tu cuenta MSN está siendo utilizada desde otro ordenador. Por favor, cierra sesión en el otro sitio para reactivar el transporte MSN."
+               msnNotVerified = u"Tu cuenta %s de MSN passport no ha sido verificada. Los usuarios de MSN no podrán ver tu nick o apodo y se les avisará de que puede que tu cuenta no sea legítima. Contacta con Microsoft para más detalles."
+               msnLoginFailure = u"El transporte MSN no ha podido iniciar sesión con la cuenta %s. Por favor, comprueba que tu contraseña sea correcta. Puede que tengas que registrarte de nuevo con el transporte."
+               msnFailedMessage = u"Este mensaje no ha podido ser entregado. Por favor, comprueba que el contacto esté conectado y que su dirección en tu lista de contactos sea correcta.\n\n"
+               msnInitialMail = u"Notificación de Hotmail\n\nMensajes sin leer en la bandeja de entrada: %s\nMensajes sin leer en otras carpetas: %s"
+               msnRealtimeMail = u"Notificación de Hotmail\n\nDe: %s <%s>\nAsunto: %s"
+               msnDisconnected = u"Desconexión de los servidores MSN: %s"
+       es_ES = es
+       es_AR = es
+       es_BO = es
+       es_CL = es
+       es_PY = es
+       es_PA = es
+       es_PE = es
+       es_UY = es
+       es_VE = es
+       es_PR = es
+       es_NI = es
+       es_MX = es
+       es_HN = es
+       es_EC = es
+       es_GT = es
+       es_CR = es
+       es_SV = es
+       es_DO = es
+       es_CR = es
+       
+
diff --git a/src/legacy/__init__.py b/src/legacy/__init__.py
new file mode 100644 (file)
index 0000000..0df7da3
--- /dev/null
@@ -0,0 +1,6 @@
+# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Licensed for distribution under the GPL version 2, check COPYING for details
+
+from glue import LegacyConnection, LegacyGroupchat, translateAccount
+from glue import name, version, mangle, id, namespace
+from glue import formRegEntry, getAttributes, isGroupJID
diff --git a/src/legacy/glue.py b/src/legacy/glue.py
new file mode 100644 (file)
index 0000000..1747492
--- /dev/null
@@ -0,0 +1,415 @@
+# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Licensed for distribution under the GPL version 2, check COPYING for details
+
+import utils
+from twisted.internet import task
+if(utils.checkTwisted()):
+       from twisted.xish.domish import Element
+else:
+       from tlib.domish import Element
+from tlib import msn
+import groupchat
+import msnw
+import config
+import debug
+import lang
+
+
+
+
+name = "MSN Transport" # The name of the transport
+version = "0.9.5"      # The transport version
+mangle = True          # XDB '@' -> '%' mangling
+id = "msn"             # The transport identifier
+
+
+
+def isGroupJID(jid):
+       """ Returns True if the JID passed is a valid groupchat JID (for MSN, does not contain '%') """
+       return (jid.find('%') == -1)
+
+
+       
+# This should be set to the name space the registration entries are in, in the xdb spool
+namespace = "jabber:iq:register"
+
+
+def formRegEntry(username, password, nickname):
+       """ Returns a domish.Element representation of the data passed. This element will be written to the XDB spool file """
+       reginfo = Element((None, "query"))
+       reginfo.attributes["xmlns"] = "jabber:iq:register"
+       
+       userEl = reginfo.addElement("username")
+       userEl.addContent(username)
+       
+       passEl = reginfo.addElement("password")
+       passEl.addContent(password)
+
+       nickEl = reginfo.addElement("nick")
+       if(nickname): nickEl.addContent(nickname)
+       
+       return reginfo
+
+
+
+
+def getAttributes(base):
+       """ This function should, given a spool domish.Element, pull the username, password,
+       and nickname out of it and return them """
+       username = ""
+       password = ""
+       nickname = ""
+       for child in base.elements():
+               try:
+                       if(child.name == "username"):
+                               username = child.__str__()
+                       elif(child.name == "password"):
+                               password = child.__str__()
+                       elif(child.name == "nick"):
+                               nickname = child.__str__()
+               except AttributeError:
+                       continue
+       
+       return username, password, nickname
+
+
+
+
+
+def msn2jid(msnid):
+       """ Converts a MSN passport into a JID representation to be used with the transport """
+       return msnid.replace('@', '%') + "@" + config.jid
+
+translateAccount = msn2jid # Marks this as the function to be used in jabber:iq:gateway (Service ID Translation)
+
+def jid2msn(jid):
+       """ Converts a JID representation of a MSN passport into the original MSN passport """
+       return unicode(jid[:jid.find('@')].replace('%', '@'))
+
+
+def presence2state(show, ptype): 
+       """ Converts a Jabber presence into an MSN status code """
+       if(ptype == "unavailable"):
+               return msn.STATUS_OFFLINE
+       elif(show in [None, "online", "chat"]):
+               return msn.STATUS_ONLINE
+       elif(show in ["dnd"]):
+               return msn.STATUS_BUSY
+       elif(show in ["away", "xa"]):
+               return msn.STATUS_AWAY
+
+
+def state2presence(state):
+       """ Converts a MSN status code into a Jabber presence """
+       if(state == msn.STATUS_ONLINE):
+               return (None, None)
+       elif(state == msn.STATUS_BUSY):
+               return ("dnd", None)
+       elif(state == msn.STATUS_AWAY):
+               return ("away", None)
+       elif(state == msn.STATUS_IDLE):
+               return ("away", None)
+       elif(state == msn.STATUS_BRB):
+               return ("away", None)
+       elif(state == msn.STATUS_PHONE):
+               return ("dnd", None)
+       elif(state == msn.STATUS_LUNCH):
+               return ("away", None)
+       else:
+               return (None, "unavailable")
+
+
+
+
+# This class handles groupchats with the legacy protocol
+class LegacyGroupchat(groupchat.BaseGroupchat):
+       def __init__(self, session, resource, ID=None, existing=False, switchboardSession=None):
+               """ Possible entry points for groupchat
+                       - User starts an empty switchboard session by sending presence to a blank room
+                       - An existing switchboard session is joined by another MSN user
+                       - User invited to an existing switchboard session with more than one user
+               """
+               groupchat.BaseGroupchat.__init__(self, session, resource, ID)
+               if(not existing):
+                       self.switchboardSession = msnw.GroupchatSwitchboardSession(self, makeSwitchboard=True)
+               else:
+                       self.switchboardSession = switchboardSession
+                       
+               assert(self.switchboardSession != None)
+               
+               debug.log("LegacyGroupchat: \"%s\" created" % (self.roomJID()))
+       
+       def removeMe(self):
+               self.switchboardSession.removeMe()
+               self.switchboardSession = None
+               groupchat.BaseGroupchat.removeMe(self)
+               debug.log("LegacyGroupchat: \"%s\" destroyed" % (self.roomJID()))
+               utils.mutilateMe(self)
+       
+       def sendLegacyMessage(self, message, noerror):
+               debug.log("LegacyGroupchat: \"%s\" sendLegacyMessage(\"%s\")" % (self.roomJID(), message))
+               self.switchboardSession.sendMessage(message, noerror)
+       
+       def sendContactInvite(self, contactJID):
+               debug.log("LegacyGroupchat: \"%s\" sendContactInvite(\"%s\")" % (self.roomJID(), contactJID))
+               userHandle = jid2msn(contactJID)
+               self.switchboardSession.inviteUser(userHandle)
+
+
+
+# This class handles most interaction with the legacy protocol
+class LegacyConnection(msnw.MSNConnection):
+       """ A glue class that connects to the legacy network """
+       def __init__(self, username, password, session):
+               self.session = session
+               self.listSynced = False
+               self.initialListVersion = 0
+
+               # Get the latest listVersion to pass to MSNConnection
+               result = self.session.pytrans.xdb.request(self.session.jabberID, "msn:listVersion")
+               if(result):
+                       self.initialListVersion = int(str(result))
+
+               # Init the MSN bits
+               msnw.MSNConnection.__init__(self, username, password)
+
+               # User typing notification stuff
+               self.userTyping = dict() # Indexed by contact MSN ID, stores whether the user is typing to this contact
+               # Contact typing notification stuff
+               self.contactTyping = dict() # Indexed by contact MSN ID, stores an integer that is incremented at 5 second intervals. If it reaches 3 then the contact has stopped typing. It is set to zero whenever MSN typing notification messages are received
+               # Looping function
+               self.userTypingSend = task.LoopingCall(self.sendTypingNotifications)
+               self.userTypingSend.start(5.0)
+               
+               import subscription # Is in here to prevent an ImportError loop
+               self.subscriptions = subscription.SubscriptionManager(self.session)
+       
+               debug.log("LegacyConnection: \"%s\" - created" % (self.session.jabberID))
+       
+       def removeMe(self):
+               debug.log("LegacyConnection: \"%s\" - being deleted" % (self.session.jabberID))
+       
+               self.userTypingSend.stop()
+       
+               if(self.getContacts()):
+                       for userHandle in self.getContacts().getContacts():
+                               msnContact = self.getContacts().getContact(userHandle)
+                               if(msnContact.status and msnContact.status != msn.STATUS_OFFLINE):
+                                       msnContact.status = msn.STATUS_OFFLINE
+                                       self.sendMSNContactPresence(msnContact)
+               
+#              msnw.MSNConnection.changeStatus(self, msn.STATUS_OFFLINE, self.session.nickname) # Change our nickname on the MSN network (stops users from appearing offline with a nickname "james - Online")
+               
+               # Save to XDB the current list version (for fast logins)
+               if(self.notificationFactory and self.notificationFactory.contacts):
+                       listVersion = self.notificationFactory.contacts.version
+                       el = Element((None, "query"))
+                       el.addContent(str(listVersion))
+                       self.session.pytrans.xdb.set(self.session.jabberID, "msn:listVersion", el)
+               
+               msnw.MSNConnection.removeMe(self)
+               self.subscriptions.removeMe()
+               self.subscriptions = None
+               self.session = None
+
+               utils.mutilateMe(self)
+       
+       def jidRes(self, resource):
+               to = self.session.jabberID
+               if(resource):
+                       to += "/" + resource
+
+               return to
+       
+       def highestResource(self):
+               """ Returns highest priority resource """
+               return self.session.highestResource()
+       
+       def sendMessage(self, dest, resource, body, noerror):
+               dest = jid2msn(dest)
+               if(self.userTyping.has_key(dest)):
+                       del self.userTyping[dest]
+               msnw.MSNConnection.sendMessage(self, dest, resource, body, noerror)
+       
+       def buildFriendly(self, status):
+               """ Constructs a friendly name from the user's registered nick, and their status message """
+               
+               if(not config.fancyFriendly):
+                       if(self.session.nickname and len(self.session.nickname) > 0):
+                               return self.session.nickname
+                       else:
+                               return self.session.jabberID[:self.session.jabberID.find('@')]
+               
+               if(self.session.nickname and len(self.session.nickname) > 0):
+                       friendly = self.session.nickname
+               else:
+                       friendly = self.session.jabberID[:self.session.jabberID.find('@')]
+               if(status and len(status) > 0):
+                       friendly += " - "
+                       friendly += status
+               if(len(friendly) > 127):
+                       friendly = friendly[:124] + "..."
+               debug.log("LegacyConnection: buildFriendly(%s) returning \"%s\"" % (self.session.jabberID, friendly))
+               return friendly
+       
+       def msnAlert(self, text, actionurl, subscrurl):
+               el = Element((None, "message"))
+               el.attributes["to"] = self.session.jabberID
+               el.attributes["from"] = config.jid
+               el.attributes["type"] = "headline"
+               body = el.addElement("body")
+               body.addContent(text)
+               
+               x = el.addElement("x")
+               x.attributes["xmlns"] = "jabber:x:oob"
+               x.addElement("desc").addContent("More information on this notice.")
+               x.addElement("url").addContent(actionurl)
+
+               x = el.addElement("x")
+               x.attributes["xmlns"] = "jabber:x:oob"
+               x.addElement("desc").addContent("Manage subscriptions to alerts.")
+               x.addElement("url").addContent(subscrurl)
+
+               self.session.pytrans.send(el)
+       
+       def setStatus(self, show, status):
+               statusCode = presence2state(show, None)
+               msnw.MSNConnection.changeStatus(self, statusCode, self.buildFriendly(status))
+       
+       def newResourceOnline(self, resource):
+               self.sendLists(resource)
+       
+       def jabberSubscriptionReceived(self, source, subtype):
+               self.subscriptions.jabberSubscriptionReceived(source, subtype)
+       
+       def sendTypingNotifications(self):
+               # Send any typing notification messages to the user's contacts
+               for contact in self.userTyping.keys():
+                       if(self.userTyping[contact]):
+                               self.sendTypingToContact(contact)
+
+               # Send any typing notification messages from contacts to the user
+               for contact, resource in self.contactTyping.keys():
+                       self.contactTyping[(contact, resource)] += 1
+                       if(self.contactTyping[(contact, resource)] >= 3):
+                               self.session.sendTypingNotification(self.jidRes(resource), msn2jid(contact), False)
+                               del self.contactTyping[(contact, resource)]
+       
+       def gotContactTyping(self, contact, resource):
+               # Check if the contact has only just started typing
+               if(not self.contactTyping.has_key((contact, resource))):
+                       self.session.sendTypingNotification(self.jidRes(resource), msn2jid(contact), True)
+
+               # Reset the counter
+               self.contactTyping[(contact, resource)] = 0
+       
+       def userTypingNotification(self, dest, resource, composing):
+               dest = jid2msn(dest)
+               self.userTyping[dest] = composing
+               if(composing): # Make it instant
+                       self.sendTypingToContact(dest)
+       
+       
+       def sendMSNContactPresence(self, msnContact, to=None):
+               if(not to):
+                       to = self.session.jabberID
+               source = msn2jid(msnContact.userHandle)
+               show, ptype = state2presence(msnContact.status)
+               status = msnContact.screenName.decode("utf-8")
+               self.session.sendPresence(to=to, fro=source, show=show, status=status, ptype=ptype)
+       
+       def sendMSNUserPresence(self, userHandle, to=None):
+               msnContact = self.getContacts().getContact(userHandle)
+               if(msnContact):
+                       self.sendMSNContactPresence(msnContact, to)
+       
+       def sendLists(self, resource):
+               """ Sends a copy of the MSN contact presences to this resource """
+               debug.log("LegacyConnection: \"%s\" - sendLists(\"%s\")" % (self.session.jabberID, resource))
+               fulljid = self.session.jabberID
+               if(resource):
+                       fulljid += "/" + resource
+               if(self.getContacts()):
+                       for userHandle in self.getContacts().getContacts():
+                               self.sendMSNUserPresence(userHandle, fulljid)
+       
+       def listSynchronized(self):
+               if(self.session):
+                       self.session.sendPresence(to=self.session.jabberID, fro=config.jid)
+                       self.subscriptions.syncJabberLegacyLists()
+                       self.listSynced = True
+                       self.subscriptions.flushSubscriptionBuffer()
+       
+       def gotMessage(self, remoteUser, resource, text):
+               source = msn2jid(remoteUser)
+               self.session.sendMessage(self.jidRes(resource), fro=source, body=text, mtype="chat")
+       
+       def loggedIn(self):
+               if(self.session):
+                       debug.log("LegacyConnection: \"%s\" - loggedIn()" % (self.session.jabberID))
+                       self.session.ready = True
+       
+       def contactStatusChanged(self, remoteUser):
+               if(self.session): # Make sure the transport isn't shutting down
+                       debug.log("LegacyConnection: \"%s\" - contactStatusChanged(\"%s\")" % (self.session.jabberID, remoteUser))
+                       self.sendMSNUserPresence(remoteUser)
+       
+       def ourStatusChanged(self, statusCode):
+               # Send out a new presence packet to the Jabber user so that the MSN-t icon changes
+               if(self.session):
+                       source = config.jid
+                       to = self.session.jabberID
+                       show, ptype = state2presence(statusCode)
+                       debug.log("LegacyConnection: \"%s\" - ourStatusChanged(\"%s\")" % (self.session.jabberID, statusCode))
+                       self.session.sendPresence(to=to, fro=source, show=show)
+       
+       def userMapping(self, passport, jid):
+               text = lang.get(self.session.lang).userMapping % (passport, jid)
+               self.session.sendMessage(to=self.session.jabberID, fro=msn2jid(passport), body=text)
+       
+       def userAddedMe(self, userHandle):
+               self.subscriptions.msnContactAddedMe(userHandle)
+       
+       def userRemovedMe(self, userHandle):
+               self.subscriptions.msnContactRemovedMe(userHandle)
+       
+       def serverGoingDown(self):
+               if(self.session):
+                       self.session.sendMessage(to=self.session.jabberID, fro=config.jid, body=lang.get(self.session.lang).msnMaintenance)
+       
+       def multipleLogin(self):
+               if(self.session):
+                       self.session.sendMessage(to=self.session.jabberID, fro=config.jid, body=lang.get(self.session.lang).msnMultipleLogin)
+                       self.session.removeMe()
+       
+       def accountNotVerified(self):
+               if(self.session):
+                       text = lang.get(self.session.lang).msnNotVerified % (self.session.username)
+                       self.session.sendMessage(to=self.session.jabberID, fro=config.jid, body=text)
+       
+       def loginFailure(self, message):
+               if(self.session):
+                       text = lang.get(self.session.lang).msnLoginFailure % (self.session.username)
+                       self.session.sendErrorMessage(to=self.session.jabberID, fro=config.jid, etype="auth", condition="not-authorized", explanation=text, body="Login Failure")
+       
+       def failedMessage(self, remoteUser, message):
+               if(self.session):
+                       fro = msn2jid(remoteUser)
+                       self.session.sendErrorMessage(to=self.session.jabberID, fro=fro, etype="wait", condition="recipient-unavailable", explanation=lang.get(self.session.lang).msnFailedMessage, body=message)
+       
+       def initialEmailNotification(self, inboxunread, foldersunread):
+               text = lang.get(self.session.lang).msnInitialMail % (inboxunread, foldersunread)
+               self.session.sendMessage(to=self.session.jabberID, fro=config.jid, body=text, mtype="headline")
+       
+       def realtimeEmailNotification(self, mailfrom, fromaddr, subject):
+               text = lang.get(self.session.lang).msnRealtimeMail % (mailfrom, fromaddr, subject)
+               self.session.sendMessage(to=self.session.jabberID, fro=config.jid, body=text, mtype="headline")
+               
+       def connectionLost(self, reason):
+               if(self.session):
+                       debug.log("LegacyConnection: \"%s\" - connectionLost(\"%s\")" % (self.session.jabberID, reason))
+                       text = lang.get(self.session.lang).msnDisconnected % ("Error") # FIXME, a better error would be nice =P
+                       self.session.sendMessage(to=self.session.jabberID, fro=config.jid, body=text)
+                       self.session.removeMe() # Tear down the session
+
+
diff --git a/src/legacy/msnw.py b/src/legacy/msnw.py
new file mode 100644 (file)
index 0000000..85b9256
--- /dev/null
@@ -0,0 +1,745 @@
+# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Licensed for distribution under the GPL version 2, check COPYING for details
+
+from twisted.internet import reactor
+from twisted.internet.protocol import ClientFactory
+from tlib import msn
+import config
+import utils
+import debug
+
+
+
+
+class MSNConnection:
+       """ Manages all the Twisted factories, etc """
+       def __init__(self, username, password):
+               self.username = username
+               self.password = password
+               self.inited = False
+               self.tries = 0
+               self.initMe()
+               debug.log("MSNConnection: \"%s\" created" % (self.username))
+               
+       def initMe(self):
+               if(self.inited):
+                       MSNConnection.removeMe(self)
+               
+               self.switchboardSessions = {}
+               dispatchFactory = DispatchFactory(self)
+               reactor.connectTCP('messenger.hotmail.com', 1863, dispatchFactory)
+               self.notificationFactory = msn.NotificationFactory()
+               self.notificationFactory.userHandle = self.username
+               self.notificationFactory.password = self.password
+               self.notificationFactory.msncon = self
+               self.notificationFactory.protocol = Notification
+               self.notificationFactory.initialListVersion = self.initialListVersion
+               self.notificationProtocol = None
+               
+               self.savedStatus = None
+               
+               self.inited = True
+               
+               debug.log("MSNConnection: \"%s\" initialised" % (self.username))
+       
+       def removeMe(self):
+               debug.log("MSNConnection: \"%s\" destroyed" % (self.username))
+               if(self.notificationProtocol):
+                       self.notificationProtocol.removeMe()
+               if(self.notificationFactory):
+                       self.notificationFactory.msncon = None
+               self.notificationFactory = None
+               self.notificationProtocol = None
+               for userHandle in utils.copyDict(self.switchboardSessions):
+                       self.switchboardSessions[userHandle].removeMe()
+               self.switchboardSessions = {}
+       
+       def resourceOffline(self, offlineResource):
+               for contact in self.switchboardSessions.keys():
+                       if(self.switchboardSessions[contact].resource == offlineResource):
+                               self.switchboardSessions[contact].resource = self.highestResource()
+       
+       def getContacts(self):
+               if(self.notificationFactory):
+                       return self.notificationFactory.contacts
+               else:
+                       return None
+               
+       
+       def sendMessage(self, remoteUser, resource, text, noerror):
+               debug.log("MSNConnection: \"%s\" sendMessage(\"%s\", \"%s\")" % (self.username, remoteUser, text))
+               if(self.notificationProtocol):
+                       if(not self.switchboardSessions.has_key(remoteUser)):
+                               self.switchboardSessions[remoteUser] = SwitchboardSession(self, remoteUser, resource)
+                       self.switchboardSessions[remoteUser].resource = resource
+                       self.switchboardSessions[remoteUser].sendMessage(text.replace("\n", "\r\n"), noerror)
+               elif(not noerror):
+                       self.failedMessage(remoteUser, text)
+       
+       def sendTypingToContact(self, remoteUser):
+               if(self.switchboardSessions.has_key(remoteUser)):
+                       self.switchboardSessions[remoteUser].sendTypingNofication()
+       
+       def notificationProtocolReady(self, notificationProtocol):
+               self.notificationProtocol = notificationProtocol
+               self.loggedIn()
+               self.tries = 0
+       
+       def sendSavedStatus(self):
+               # Hack for initial status
+               if(self.savedStatus):
+                       statusCode, screenName = self.savedStatus
+                       self.savedStatus = None
+                       self.changeStatus(statusCode, screenName)
+       
+       def changeStatus(self, statusCode, screenName):
+               if(self.notificationProtocol):
+                       def cb1(arg):
+                               self.ourStatusChanged(arg[0])
+                       def cb2(arg):
+                               self.ourNickChanged(arg[0])
+                       debug.log("MSNConnection: \"%s\" - changing status and screenName (\"%s\", \"%s\")" % (self.username, statusCode, screenName))
+                       if(statusCode):
+                               statusCode = str(statusCode.encode("utf-8"))
+                               self.notificationProtocol.changeStatus(statusCode).addCallback(cb1)
+                       if(screenName):
+                               screenName = str(screenName.encode("utf-8"))
+                               self.notificationProtocol.changeScreenName(screenName).addCallback(cb2)
+               else:
+                       self.savedStatus = (statusCode, screenName)
+       
+       def connectionLostBase(self, reason):
+               # Attempts to reconnect
+               if(self.tries < 5 and self.session):
+                       reactor.callLater(2 ** self.tries, self.initMe)
+                       self.tries += 1
+               else:
+                       self.connectionLost(self)
+       
+       def addContact(self, listType, userHandle):
+               return self.notificationProtocol.addContact(listType, str(userHandle))
+       
+       def remContact(self, listType, userHandle, groupID=0):
+               return self.notificationProtocol.remContact(listType, str(userHandle))
+       
+       
+       
+       def initialEmailNotification(self, inboxunread, foldersunread):
+               pass
+       
+       def realtimeEmailNotification(self, mailfrom, fromaddr, subject):
+               pass
+       
+       def loggedIn(self):
+               pass
+       
+       def loginFailure(self, message):
+               pass
+               
+       def multipleLogin(self):
+               pass
+       
+       def gotMessage(self, remoteUser, resource, text):
+               pass
+       
+       def listSynchronized(self):
+               pass
+       
+       def contactStatusChanged(self, remoteUser):
+               pass
+       
+       def ourStatusChanged(self, statusCode):
+               pass
+       
+       def userMapping(self, passport, jid):
+               pass
+       
+       def gotContactTyping(self, remoteUser, resource):
+               pass
+       
+       def ourNickChanged(self, arg):
+               pass
+       
+       def serverGoingDown(self):
+               pass
+       
+       def accountNotVerified(self):
+               pass
+       
+       def userAddedMe(self, userHandle):
+               pass
+       
+       def userRemovedMe(self, userHandle):
+               pass
+       
+       def failedMessage(self, remoteUser, message):
+               pass
+       
+       def connectionLost(self):
+               pass
+
+
+
+
+def switchToGroupchat(switchboardSession, user1, user2):
+       gcsbs = GroupchatSwitchboardSession()
+       from glue import LegacyGroupchat, msn2jid
+       groupchat = LegacyGroupchat(session=switchboardSession.msncon.session, resource=None, existing=True, switchboardSession=gcsbs)
+       gcsbs.groupchat = groupchat
+       gcsbs.msncon = switchboardSession.msncon
+       gcsbs.switchboard = switchboardSession.switchboard
+       gcsbs.switchboard.switchboardSession = gcsbs
+       gcsbs.ready = True
+       gcsbs.userJoined(user1)
+       gcsbs.userJoined(user2)
+       groupchat.sendUserInvite(msn2jid(switchboardSession.remoteUser))
+       switchboardSession.removeMe(False)
+       debug.log("GroupchatSwitchboardSession: \"%s\" \"%s\" created by conversion" % (gcsbs.groupchat.roomJID(), gcsbs))
+       return gcsbs
+
+
+class GroupchatSwitchboardSession:
+       def __init__(self, groupchat=None, makeSwitchboard=False):
+               self.removed = False
+
+               self.msncon = None
+               self.groupchat = None
+               if(groupchat):
+                       self.groupchat = groupchat
+                       self.msncon = self.groupchat.session.legacycon
+               self.switchboard = None
+               self.ready = False
+               self.messageBuffer = []
+               self.invitedUsers = []
+               self.oneUserHasJoined = False
+               
+               if(makeSwitchboard and groupchat):
+                       debug.log("GroupchatSwitchboardSession: \"%s\" \"%s\" requesting a switchboard session" % (self.groupchat.roomJID(), self))
+                       d = self.msncon.notificationProtocol.requestSwitchboardServer()
+                       d.addCallback(self.sbRequestAccepted)
+                       d.addErrback(self.removeMe)
+               
+               if(self.msncon):
+                       debug.log("GroupchatSwitchboardSession: \"%s\" \"%s\" created" % (self.msncon.username, self))
+       
+       def removeMe(self):
+               if(self.removed):
+                       debug.log("GroupchatSwitchboardSession: removeMe called more than once! Traceback!")
+                       return
+               self.removed = True
+
+               debug.log("GroupchatSwitchboardSession: \"%s\" \"%s\" destroyed" % (self.groupchat.roomJID(), self))
+               self.msncon = None
+               if(self.switchboard):
+                       self.switchboard.removeMe()
+               self.switchboard = None
+               self.groupchat = None
+               self.ready = False
+
+               utils.mutilateMe(self)
+       
+       def sbRequestAccepted(self, (host, port, key)):
+               # Connect to the switchboard server
+               debug.log("GroupchatSwitchboardSession: \"%s\" \"%s\" sbRequestAccepted()" % (self.msncon.username, self))
+               reactor.connectTCP(host, port, SwitchboardFactory(self, key))
+       
+       def sendMessage(self, message, noerror):
+               if(self.ready and self.oneUserHasJoined):
+                       def failedMessage(ignored=None):
+                               tempmsncon.failedMessage(self.groupchat.roomJID(), message)
+                       message = str(message.encode("utf-8"))
+                       msnmessage = msn.MSNMessage(message=message)
+                       msnmessage.setHeader("Content-Type", "text/plain; charset=UTF-8")
+                       msnmessage.ack = msn.MSNMessage.MESSAGE_NACK
+                       tempmsncon = self.msncon # In case MSN tells us the message failed after removeMe()
+                       d = self.switchboard.sendMessage(msnmessage)
+                       if(not noerror):
+                               d.addCallback(failedMessage)
+               else:
+                       self.messageBuffer.append(message)
+       
+       def inviteUser(self, userHandle):
+               userHandle = str(userHandle)
+               if(self.ready):
+                       debug.log("GroupchatSwitchboardSession: \"%s\" \"%s\" inviting %s" % (self.msncon.username, self, userHandle))
+                       self.switchboard.inviteUser(userHandle)
+               else:
+                       self.invitedUsers.append(userHandle)
+       
+       def gotMessage(self, message):
+               self.groupchat.messageReceived(message.userHandle, message.getMessage())
+       
+       def flushBuffer(self):
+               for m in utils.copyList(self.messageBuffer):
+                       self.messageBuffer.remove(m)
+                       self.sendMessage(m, True)
+               
+               for i in utils.copyList(self.invitedUsers):
+                       self.invitedUsers.remove(i)
+                       self.inviteUser(i)
+       
+       def userJoined(self, userHandle):
+               debug.log("GroupchatSwitchboardSession: \"%s\" \"%s\" userJoined(\"%s\")" % (self.msncon.username, self, userHandle))
+               self.oneUserHasJoined = True
+               self.flushBuffer()
+               self.groupchat.contactJoined(userHandle)
+       
+       def userLeft(self, userHandle):
+               debug.log("GroupchatSwitchboardSession: \"%s\" \"%s\" userLeft(\"%s\")" % (self.msncon.username, self, userHandle))
+               self.groupchat.contactLeft(userHandle)
+
+
+
+class SwitchboardSession:
+       def __init__(self, msncon, remoteUser, resource, reply=False, host=None, port=None, key=None, sessionID=None):
+               self.removed = False
+
+               self.msncon = msncon
+               self.remoteUser = str(remoteUser)
+               self.resource = str(resource)
+               
+               self.killTimer = reactor.callLater(30.0*60.0, self.removeMe)
+               
+               self.switchboard = None # The SwitchboardClient class
+               self.messageBuffer = [] # Any messages sent before the switchboard is ready are buffered
+               self.ready = False # Is True when we are connected to the switchboard, and the remote user has accepted our invite
+               
+               if(not reply):
+                       # Request a switchboard
+                       d = self.msncon.notificationProtocol.requestSwitchboardServer()
+                       d.addCallback(self.sbRequestAccepted)
+                       d.addErrback(self.removeMe)
+               else:
+                       reactor.connectTCP(host, port, SwitchboardFactory(self, key, sessionID, reply))
+               
+               debug.log("SwitchboardSession: \"%s\" \"%s\" \"%s\" created" % (self.msncon.username, self.remoteUser, self.resource))
+       
+       def removeMe(self, sbflag=True):
+               if(self.removed):
+                       debug.log("SwitchboardSession: removeMe called more than once! Traceback!")
+                       return
+               self.removed = True
+
+               debug.log("SwitchboardSession: \"%s\" \"%s\" \"%s\" destroyed" % (self.msncon.username, self.remoteUser, self.resource))
+               for message, noerror in self.messageBuffer:
+                       if(not noerror):
+                               self.msncon.failedMessage(self.remoteUser, message)
+               self.messageBuffer = []
+
+               del self.msncon.switchboardSessions[self.remoteUser]
+               self.msncon = None
+               if(sbflag and self.switchboard):
+                       self.switchboard.removeMe()
+               self.switchboard = None
+               self.ready = False
+               if(self.killTimer and not self.killTimer.called):
+                       self.killTimer.cancel()
+               self.killTimer = None
+
+               utils.mutilateMe(self)
+       
+       def resetTimer(self):
+               # Sets a count down timer to kill this switchboard session in 30 minutes
+               self.killTimer.cancel()
+               self.killTimer = reactor.callLater(30.0*60.0, self.removeMe)
+       
+       def sbRequestAccepted(self, (host, port, key)):
+               # Connect to the switchboard server
+               reactor.connectTCP(host, port, SwitchboardFactory(self, key))
+       
+       def sendMessage(self, message, noerror):
+               if(self.ready):
+                       debug.log("SwitchboardSession: \"%s\" \"%s\" sending message \"%s\"" % (self.msncon.username, self.remoteUser, message))
+                       message = str(message.encode("utf-8"))
+                       msnmessage = msn.MSNMessage(message=message)
+                       msnmessage.setHeader("Content-Type", "text/plain; charset=UTF-8")
+                       msnmessage.ack = msn.MSNMessage.MESSAGE_NACK
+                       def failedMessage(ignored):
+                               tempmsncon.failedMessage(self.remoteUser, message)
+                       d = self.switchboard.sendMessage(msnmessage)
+                       tempmsncon = self.msncon # In case MSN tells us the message failed after removeMe()
+                       if(not noerror):
+                               d.addCallback(failedMessage)
+                       self.resetTimer()
+               else:
+                       self.messageBuffer.append((message, noerror))
+       
+       def sendTypingNofication(self):
+               if(self.ready):
+                       self.switchboard.sendTypingNotification()
+       
+       def contactTyping(self):
+               self.msncon.gotContactTyping(self.remoteUser, self.resource)
+       
+       def flushBuffer(self):
+               for m, noerror in utils.copyList(self.messageBuffer):
+                       self.messageBuffer.remove((m, noerror))
+                       self.sendMessage(m, noerror)
+       
+       def gotMessage(self, message):
+               self.msncon.gotMessage(self.remoteUser, self.resource, message.getMessage())
+               self.resetTimer()
+       
+       def userJoined(self, userHandle):
+               if(userHandle != self.remoteUser):
+                       # Another user has joined, so we now have three participants (these two and ourself)
+                       switchToGroupchat(self, self.remoteUser, userHandle)
+       
+       def userLeft(self, userHandle):
+               if(userHandle == self.remoteUser):
+                       self.removeMe()
+
+
+
+
+
+
+class DispatchFactory(ClientFactory):
+       def __init__(self, msncon):
+               self.msncon = msncon
+       
+       def buildProtocol(self, addr):
+               p = Dispatch(self.msncon)
+               del self.msncon # No longer needed
+               return p
+
+
+class Dispatch(msn.DispatchClient):
+       def __init__(self, msncon):
+               msn.DispatchClient.__init__(self)
+               self.msncon = msncon
+               self.userHandle = self.msncon.username
+       
+       def __del__(self):
+               self.factory = None
+               self.msncon = None
+       
+       def gotNotificationReferral(self, host, port):
+               self.transport.loseConnection()
+               if(self.msncon and self.msncon.session and self.msncon.session.alive):
+                       reactor.connectTCP(host, port, self.msncon.notificationFactory)
+
+
+
+class Notification(msn.NotificationClient):
+       def __init__(self):
+               self.removed = False
+
+               msn.NotificationClient.__init__(self, proxy=config.proxyServer, proxyport=config.proxyPort)
+
+       def removeMe(self):
+               if(self.removed):
+                       debug.log("Notification: removeMe called more than once! Traceback!")
+                       return
+               self.removed = True
+
+               self.logOut()
+               self.transport.loseConnection()
+               if(self.factory.msncon):
+                       self.factory.msncon.notificationProtocol = None
+                       self.factory.msncon = None
+               self.factory = None
+
+               utils.mutilateMe(self)
+       
+       def badConditions(self):
+               if(not (self.factory and self.factory.msncon and self.factory.msncon.session and self.factory.msncon.session.alive)):
+                       if(not self.removed):
+                               self.removeMe()
+                       return True
+               return False
+               
+       
+       def loginFailure(self, message):
+               if(self.badConditions()): return
+               self.factory.msncon.loginFailure(message)
+       
+       def loggedIn(self, userHandle, screenName, verified):
+               if(self.badConditions()): return
+
+               self.factory.msncon.notificationProtocolReady(self)
+               if(not verified):
+                       self.factory.msncon.accountNotVerified()
+               
+               msn.NotificationClient.loggedIn(self, userHandle, screenName, verified)
+               
+               debug.log("NotificationClient: \"%s\" authenticated with MSN servers" % (self.factory.msncon.username))
+       
+       def gotMessage(self, msnmessage):
+               if(self.badConditions()): return
+               debug.log("NotificationClient: \"%s\" gotMessage()" % (self.factory.msncon.username))
+       
+               cTypes = [s.lstrip() for s in msnmessage.getHeader("Content-Type").split(';')]
+               def getFields():
+                       fields = msnmessage.getMessage().strip().split('\n')
+                       values = {}
+                       for i in fields:
+                               a = i.split(':')
+                               if(len(a) != 2): continue
+                               f, v = a
+                               f = f.strip()
+                               v = v.strip()
+                               values[f] = v
+                       return values
+               
+               if("text/x-msmsgsinitialemailnotification" in cTypes and config.mailNotifications):
+                       values = getFields()
+                       try:
+                               inboxunread = int(values["Inbox-Unread"])
+                               foldersunread = int(values["Folders-Unread"])
+                       except KeyError:
+                               return
+                       if(foldersunread + inboxunread == 0): return # For some reason MSN sends notifications about empty inboxes sometimes?
+                       debug.log("NotificationClient: \"%s\" Initial hotmail notification" % (self.factory.msncon.username))
+                       self.factory.msncon.initialEmailNotification(inboxunread, foldersunread)
+               
+               elif("text/x-msmsgsemailnotification" in cTypes and config.mailNotifications):
+                       values = getFields()
+                       try:
+                               mailfrom = values["From"]
+                               fromaddr = values["From-Addr"]
+                               subject = values["Subject"]
+                               junkbeginning = "=?\"us-ascii\"?Q?"
+                               junkend = "?="
+                               subject = subject.replace(junkbeginning, "").replace(junkend, "").replace("_", " ")
+                       except KeyError:
+                               # If any of the fields weren't found then it's not a big problem. We just ignore the message
+                               return
+                       debug.log("NotificationClient: \"%s\" Live hotmail notification" % (self.factory.msncon.username))
+                       self.factory.msncon.realtimeEmailNotification(mailfrom, fromaddr, subject)
+
+               elif("NOTIFICATION" == msnmessage.userHandle):
+                       notification = utils.parseText(msnmessage.message)
+                       siteurl = notification.getAttribute("siteurl")
+                       notid = notification.getAttribute("id")
+
+                       msg = None
+                       for e in notification.elements():
+                               if(e.name == "MSG"):
+                                       msg = e
+                                       break
+                       else: return
+
+                       msgid = msg.getAttribute("id")
+
+                       action = None
+                       subscr = None
+                       bodytext = None
+                       for e in msg.elements():
+                               if(e.name == "ACTION"):
+                                       action = e.getAttribute("url")
+                               if(e.name == "SUBSCR"):
+                                       subscr = e.getAttribute("url")
+                               if(e.name == "BODY"):
+                                       for e2 in e.elements():
+                                               if(e2.name == "TEXT"):
+                                                       bodytext = e2.__str__()
+                       if(not (action and subscr and bodytext)): return
+
+
+                       actionurl = "%s&notification_id=%s&message_id=%s&agent=messenger" % (action, notid, msgid)
+                       subscrurl = "%s&notification_id=%s&message_id=%s&agent=messenger" % (subscr, notid, msgid)
+                       
+                       self.factory.msncon.msnAlert(bodytext, actionurl, subscrurl)
+
+       
+       
+       def connectionLost(self, reason):
+               if(self.badConditions()): return
+               def wait():
+                       debug.log("NotificationClient: \"%s\" lost connection with MSN servers" % (self.factory.userHandle))
+                       msn.NotificationClient.connectionLost(self, reason)
+                       self.factory.msncon.connectionLostBase(reason)
+               # Make sure this event is handled after any others
+               reactor.callLater(0, wait)
+       
+       def listSynchronized(self, *args):
+               if(self.badConditions()): return
+               debug.log("NotificationClient: \"%s\" MSN contact lists synchronised" % (self.factory.userHandle))
+               self.factory.msncon.listSynchronized()
+               if(self.badConditions()): return # Just in case the session is deregistered
+               self.factory.msncon.sendSavedStatus()
+       
+       def gotSwitchboardInvitation(self, sessionID, host, port, key, remoteUser, screenName):
+               if(self.badConditions()): return
+               debug.log("NotificationClient: \"%s\" gotSwitchboardInvitation(\"%s\")" % (self.factory.userHandle, remoteUser))
+               sbs = SwitchboardSession(self.factory.msncon, remoteUser, self.factory.msncon.session.highestResource(), True, host, port, key, sessionID)
+               if(self.factory.msncon.switchboardSessions.has_key(remoteUser)):
+                       self.factory.msncon.switchboardSessions[remoteUser].removeMe()
+               self.factory.msncon.switchboardSessions[remoteUser] = sbs
+       
+       def contactStatusChanged(self, statusCode, userHandle, screenName):
+               if(self.badConditions()): return
+               debug.log("NotificationClient: \"%s\" contactStatusChanged(\"%s\", \"%s\")" % (self.factory.userHandle, statusCode, userHandle))
+               msn.NotificationClient.contactStatusChanged(self, statusCode, userHandle, screenName)
+               
+               self.factory.msncon.contactStatusChanged(userHandle)
+       
+       def gotContactStatus(self, statusCode, userHandle, screenName):
+               if(self.badConditions()): return
+               msn.NotificationClient.gotContactStatus(self, statusCode, userHandle, screenName)
+               debug.log("NotificationClient: \"%s\" gotContactStatus(\"%s\", \"%s\")" % (self.factory.userHandle, statusCode, userHandle))
+               
+               self.factory.msncon.contactStatusChanged(userHandle)
+       
+       def contactOffline(self, userHandle):
+               if(self.badConditions()): return
+               debug.log("NotificationClient: \"%s\" contactOffline(\"%s\")" % (self.factory.userHandle, userHandle))
+               msn.NotificationClient.contactOffline(self, userHandle)
+               
+               self.factory.msncon.contactStatusChanged(userHandle)
+       
+       def userAddedMe(self, userHandle, screenName, listVersion):
+               if(self.badConditions()): return
+               debug.log("NotificationClient: \"%s\" userAddedMe(\"%s\", \"%s\")" % (self.factory.userHandle, userHandle, listVersion))
+               msn.NotificationClient.userAddedMe(self, userHandle, screenName, listVersion)
+               self.factory.msncon.userAddedMe(userHandle)
+       
+       def userRemovedMe(self, userHandle, listVersion):
+               if(self.badConditions()): return
+               debug.log("NotificationClient: \"%s\" userRemovedMe(\"%s\", \"%s\")" % (self.factory.userHandle, userHandle, listVersion))
+               msn.NotificationClient.userRemovedMe(self, userHandle, listVersion)
+               self.factory.msncon.userRemovedMe(userHandle)
+       
+       def multipleLogin(self):
+               if(self.badConditions()): return
+               debug.log("NotificationClient: \"%s\" multiple logins" % (self.factory.msncon.username))
+               self.factory.msncon.multipleLogin()
+
+
+
+class SwitchboardFactory(ClientFactory):
+       def __init__(self, switchboardSession, key, sessionID=None, reply=False):
+               self.switchboardSession = switchboardSession
+               self.key = key
+               self.sessionID = sessionID
+               self.reply = reply
+       
+       def buildProtocol(self, addr):
+               p = Switchboard(self.switchboardSession)
+               if(p.badConditions()): return p
+               p.key = self.key
+               p.sessionID = self.sessionID
+               p.reply = self.reply
+               p.userHandle = self.switchboardSession.msncon.username
+               p.factory = self
+               return p
+
+class Switchboard(msn.SwitchboardClient):
+       def __init__(self, switchboardSession):
+               self.removed = False
+
+               msn.SwitchboardClient.__init__(self)
+               self.switchboardSession = switchboardSession
+               self.chattingUsers = []
+               self.callid = None
+               if(self.badConditions()): return
+               debug.log("SwitchboardClient: \"%s\" \"%s\" - created" % (self.switchboardSession.msncon.username, self.switchboardSession))
+       
+       def removeMe(self):
+               if(self.removed):
+                       debug.log("Switchboard: removeMe called more than once! Traceback!")
+                       return
+               self.removed = True
+
+               self.transport.loseConnection()
+               debug.log("SwitchboardClient: \"%s\" - destroyed" % (self.switchboardSession))
+               self.switchboardSession = None
+               self.factory.switchboardSession = None
+               self.factory = None
+
+               if(self.callid and not self.callid.called):
+                       self.callid.cancel() # Cancel the invite fail message
+               self.callid = None
+
+               utils.mutilateMe(self)
+       
+       def badConditions(self):
+               if(not (self.switchboardSession and self.switchboardSession.msncon and self.switchboardSession.msncon.session and self.switchboardSession.msncon.session.alive)):
+                       if(self.switchboardSession):
+                               if(not self.switchboardSession.removed):
+                                       self.switchboardSession.removeMe()
+                       elif(not self.removed):
+                               self.removeMe()
+                       return True
+               return False
+       
+       def loggedIn(self):
+               if(self.badConditions()): return
+               if((not self.reply) and self.switchboardSession.__class__ == SwitchboardSession):
+                       def failCB(arg=None):
+                               debug.log(templogmessage)
+                               self.switchboardSession.removeMe()
+                       d = self.inviteUser(self.switchboardSession.remoteUser)
+                       d.addErrback(failCB)
+                       templogmessage = "SwitchboardClient: \"%s\" \"%s\" - user has NOT joined after 30 seconds" % (self.switchboardSession.msncon.username, self.switchboardSession.remoteUser)
+                       # If the user doesn't join then we want to tear down the SwitchboardSession
+                       self.callid = reactor.callLater(30.0, failCB)
+               
+               else:
+                       self.readySwitchboardSession()
+       
+       def readySwitchboardSession(self, ignored=None):
+               if(self.badConditions()): return
+               debug.log("SwitchboardClient: \"%s\" \"%s\" - ready for use" % (self.switchboardSession.msncon.username, self.switchboardSession))
+               self.switchboardSession.ready = True
+               self.switchboardSession.switchboard = self
+               self.switchboardSession.flushBuffer()
+               for user in self.chattingUsers:
+                       self.switchboardSession.userJoined(user)
+               if(self.callid and not self.callid.called):
+                       self.callid.cancel() # Cancel the invite fail message (only applies if we needed to invite the user)
+               self.callid = None
+       
+       def gotChattingUsers(self, users):
+               for user in users:
+                       self.chattingUsers.append(user)
+       
+       def userJoined(self, userHandle, screenName):
+               if(self.badConditions()): return
+               if((not self.reply) and self.switchboardSession.__class__ == SwitchboardSession):
+                       self.readySwitchboardSession()
+               debug.log("SwitchboardClient: \"%s\" \"%s\" - userJoined(\"%s\")" % (self.switchboardSession.msncon.username, self.switchboardSession, userHandle))
+               self.switchboardSession.userJoined(userHandle)
+               self.sendClientCaps()
+       
+       def userLeft(self, userHandle):
+               if(self.badConditions()): return
+               debug.log("SwitchboardClient: \"%s\" \"%s\" - userLeft(\"%s\")" % (self.switchboardSession.msncon.username, self.switchboardSession, userHandle))
+               def wait():
+                       self.switchboardSession.userLeft(userHandle)
+               # Make sure this event is handled after any others (eg, gotMessage)
+               reactor.callLater(0, wait)
+       
+       def gotMessage(self, message):
+               if(self.badConditions()):
+                       debug.log("SwitchboardClient: gotMessage called too late! Traceback!")
+                       return
+               debug.log("SwitchboardClient: \"%s\" \"%s\" gotMessage(\"%s\")" % (self.switchboardSession.msncon.username, message.userHandle, message.getMessage()))
+               cTypes = [s.lstrip() for s in message.getHeader("Content-Type").split(';')]
+               if("text/plain" in cTypes):
+                       if(len(cTypes) > 1 and cTypes[1].find("UTF-8") >= 0):
+                               message.message = message.message.decode("utf-8")
+                       self.switchboardSession.gotMessage(message)
+                       return
+               if("text/x-clientcaps" in cTypes):
+                       if(message.hasHeader("JabberID")):
+                               jid = message.getHeader("JabberID")
+                               self.switchboardSession.msncon.userMapping(message.userHandle, jid)
+                       return
+               debug.log("Discarding unknown message type: %s" % (message.getMessage()))
+       
+       def userTyping(self, message):
+               if(self.badConditions()): return
+               if(self.switchboardSession.__class__ == SwitchboardSession): # Ignore typing in groupchats
+                       if(message.userHandle == self.switchboardSession.remoteUser):
+                               self.switchboardSession.contactTyping()
+       
+       def sendClientCaps(self):
+               message = msn.MSNMessage()
+               message.setHeader("Content-Type", "text/x-clientcaps")
+               message.setHeader("Client-Name", "PyMSNt")
+               message.setHeader("JabberID", str(self.switchboardSession.msncon.session.jabberID)) # FIXME, this is a little deep
+               self.sendMessage(message)
+
diff --git a/src/legacy/subscription.py b/src/legacy/subscription.py
new file mode 100644 (file)
index 0000000..1b99972
--- /dev/null
@@ -0,0 +1,247 @@
+# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Licensed for distribution under the GPL version 2, check COPYING for details
+
+import utils
+if(utils.checkTwisted()):
+       from twisted.xish.domish import Element
+else:
+       from tlib.domish import Element
+from tlib import msn
+from legacy import glue
+import debug
+
+
+def getGroupNames(msnContact, msnContactList):
+       groups = []
+       for groupID in msnContact.groups:
+               try:
+                       groups.append(msnContactList.groups[groupID])
+               except:
+                       pass
+       return groups
+
+
+def msnlist2jabsub(lists):
+       """ Converts MSN contact lists ORed together into the corresponding Jabber subscription state """
+       if(lists & msn.FORWARD_LIST and lists & msn.REVERSE_LIST):
+               return "both"
+       elif(lists & msn.REVERSE_LIST):
+               return "from"
+       elif(lists & msn.FORWARD_LIST):
+               return "to"
+       else:
+               return "none"
+
+
+def jabsub2msnlist(sub):
+       """ Converts a Jabber subscription state into the corresponding MSN contact lists ORed together """
+       if(sub == "to"):
+               return msn.FORWARD_LIST
+       elif(sub == "from"):
+               return msn.REVERSE_LIST
+       elif(sub == "both"):
+               return (msn.FORWARD_LIST | msn.REVERSE_LIST)
+       else:
+               return 0
+
+
+
+class SubscriptionManager:
+       def __init__(self, session):
+               self.session = session
+               self.subscriptionBuffer = []
+       
+       def removeMe(self):
+               self.subscriptionBuffer = None
+               self.session = None
+               
+       def syncJabberLegacyLists(self):
+               """ Synchronises the MSN contact list on server with the Jabber contact list """
+               # We have to make an MSNContactList from the XDB data, then compare it with the one the server sent
+               # Any subscription changes must be sent to the client, as well as changed in the XDB
+               debug.log("Subscriptions: Session \"%s\" starting syncJabberLegacyLists()" % (self.session.jabberID))
+               result = self.session.pytrans.xdb.request(self.session.jabberID, "jabber:iq:roster")
+               oldContactList = msn.MSNContactList()
+               if(result):
+                       for item in result.elements():
+                               user = item.getAttribute("jid")
+                               sub = item.getAttribute("subscription")
+                               lists = jabsub2msnlist(sub)
+                               contact = msn.MSNContact(userHandle=user, screenName="", lists=lists)
+                               oldContactList.addContact(contact)
+               
+               newXDB = Element((None, "query"))
+               newXDB.attributes["xmlns"] = "jabber:iq:roster"
+               
+               contactList = self.session.legacycon.getContacts()
+               for contact in contactList.contacts.values():
+                       # Compare with the XDB <item/> entry
+                       oldContact = oldContactList.getContact(contact.userHandle)
+                       if(oldContact == None):
+                               oldLists = 0
+                       else:
+                               oldLists = oldContact.lists
+                       lists = contact.lists
+                       
+                       
+                       def updatePresence(ptype): # Convienence
+                               self.session.sendPresence(to=self.session.jabberID, fro=glue.msn2jid(contact.userHandle), ptype=ptype)
+                       
+                       if(not (oldLists & msn.FORWARD_LIST)):
+                               if(lists & msn.FORWARD_LIST):
+                                       # User has been added to forward list
+                                       groups = getGroupNames(contact, self.session.legacycon.getContacts())
+                                       self.session.sendRosterImport(glue.msn2jid(contact.userHandle), "subscribe", "both", contact.screenName, groups)
+                       else:
+                               if(not (lists & msn.FORWARD_LIST)):
+                                       # User has been removed from forward list
+                                       updatePresence("unsubscribed")
+                       
+                       if(oldLists & msn.REVERSE_LIST):
+                               if(not (lists & msn.REVERSE_LIST)):
+                                       # User has been removed from reverse list
+                                       updatePresence("unsubscribe")
+       
+                       if(not (lists & msn.ALLOW_LIST) and not (lists & msn.BLOCK_LIST) and (lists & msn.REVERSE_LIST)):
+                               # User isn't blocked, and isn't currently allowed, so will subscribe to your presence
+                               updatePresence("subscribe")
+                       
+                       item = newXDB.addElement("item")
+                       item.attributes["jid"] = contact.userHandle
+                       item.attributes["subscription"] = msnlist2jabsub(lists)
+               
+               # Update the XDB
+               self.session.pytrans.xdb.set(self.session.jabberID, "jabber:iq:roster", newXDB)
+               debug.log("Subscriptions: Session \"%s\" finished syncJabberLegacyLists()" % (self.session.jabberID))
+       
+       
+       
+       def msnContactAddedMe(self, userHandle):
+               # User has been added to our reverse list (we are now on their forward list)
+               debug.log("Subscriptions: Session \"%s\" msnContactAddedMe(\"%s\")" % (self.session.jabberID, userHandle))
+               
+               # Update the XDB if needed
+               document = self.session.pytrans.xdb.request(self.session.jabberID, "jabber:iq:roster")
+               if(not document): return # We can ignore this error. It will get fixed at next logon. It's fixed better in the next version :)
+               for item in document.elements():
+                       user = item.getAttribute("jid")
+                       if(user == userHandle):
+                               break
+               else:
+                       # Not found, so we need to add it
+                       item = document.addElement("item")
+                       item.attributes["jid"] = userHandle
+                       item.attributes["subscription"] = "from"
+                       self.session.pytrans.xdb.set(self.session.jabberID, "jabber:iq:roster", document)
+               
+               # Send a subscription packet
+               source = glue.msn2jid(userHandle)
+               self.session.sendPresence(to=self.session.jabberID, fro=source, ptype="subscribe")
+       
+       def msnContactRemovedMe(self, userHandle):
+               # User has been removed from our reverse list (we are no longer on their forward list)
+               debug.log("Subscriptions: Session \"%s\" msnContactRemovedMe(\"%s\")" % (self.session.jabberID, userHandle))
+               
+               # Update the XDB
+               document = self.session.pytrans.xdb.request(self.session.jabberID, "jabber:iq:roster")
+               for item in document.elements():
+                       user = item.getAttribute("jid")
+                       if(user == userHandle):
+                               document.children.remove(item)
+                               break
+               self.session.pytrans.xdb.set(self.session.jabberID, "jabber:iq:roster", document)
+               
+               # Send an unsubscribe packet (the contact is no longer viewing the user's presence)
+               source = glue.msn2jid(userHandle)
+               self.session.sendPresence(to=self.session.jabberID, fro=source, ptype="unsubscribe")
+       
+       
+       def flushSubscriptionBuffer(self):
+               for (to, subtype) in self.subscriptionBuffer:
+                       self.jabberSubscriptionReceived(to, subtype)
+       
+       def jabberSubscriptionReceived(self, to, subtype):
+               debug.log("Subscriptions: Session \"%s\" - jabberSubscriptionReceived(\"%s\", \"%s\")" % (self.session.jabberID, to, subtype))
+               
+               if(not (self.session.ready and self.session.legacycon.getContacts())):
+                       # Buffer until we have received the MSN contact list
+                       self.subscriptionBuffer.append((to, subtype))
+                       return
+               
+               def updatePresence(ptype): # Convienence
+                       self.session.sendPresence(to=self.session.jabberID, fro=to, ptype=ptype)
+               
+               if(to.find('@') > 0): # For contacts
+                       userHandle = glue.jid2msn(to)
+                       
+                       msnContact = self.session.legacycon.getContacts().getContact(userHandle)
+                       lists = 0 # Lists default to none
+                       if(msnContact and msnContact.lists):
+                               lists = msnContact.lists # We know extra information about what lists they're in.
+                       
+                       if(subtype == "subscribe"):
+                               # User wants to subscribe to contact's presence
+                               if(lists & msn.FORWARD_LIST):
+                                       # The contact is already on the user's MSN list, so tell the user that this is so
+                                       groups = getGroupNames(msnContact, self.session.legacycon.getContacts())
+                                       self.session.sendRosterImport(glue.msn2jid(msnContact.userHandle), "subscribed", "both", msnContact.screenName, groups)
+                               else:
+                                       # Add the contact to the user's MSN list
+                                       def cb(arg=None):
+                                               updatePresence("subscribed")
+                                       self.session.legacycon.addContact(msn.FORWARD_LIST, userHandle).addCallback(cb)
+                       
+                       elif(subtype == "subscribed"):
+                               # The user has granted this contact subscription.
+                               def remFromBlockList(arg=None):
+                                       if(lists & msn.BLOCK_LIST):
+                                               # If they were on the block list remove them
+                                               def cb(arg=None):
+                                                       pass
+                                               self.session.legacycon.remContact(msn.BLOCK_LIST, userHandle).addCallback(cb)
+                               
+                               if(not (lists & msn.ALLOW_LIST)):
+                                       # We add the contact to the allow list
+                                       self.session.legacycon.addContact(msn.ALLOW_LIST, userHandle).addCallback(remFromBlockList)
+                       
+                       elif(subtype == "unsubscribe"):
+                               # User wants to unsubscribe to this contact's presence. (User is removing the contact from their list)
+                               if(lists & msn.FORWARD_LIST):
+                                       # Contact is in the forward list, so remove them
+                                       def cb(arg=None):
+                                               pass
+                                       self.session.legacycon.remContact(msn.FORWARD_LIST, userHandle).addCallback(cb)
+                               else:
+                                       updatePresence("unsubscribed")
+                       
+                       elif(subtype == "unsubscribed"):
+                               # The user wants to remove this contact's authorisation. Contact will no longer be able to see user
+                               def addToBlockList(arg=None):
+                                       if(not (lists & msn.BLOCK_LIST)):
+                                               # If they're not on the block list then add them to it
+                                               def cb(arg=None):
+                                                       pass
+                                               self.session.legacycon.addContact(msn.BLOCK_LIST, userHandle).addCallback(cb)
+
+                               if(lists & msn.ALLOW_LIST):
+                                       # If they're currently on the allow list then remove them
+                                       # Should we add them to the block list?
+                                       # Yes we should. The MSN client seems to. So we'll follow their lead.
+                                       self.session.legacycon.remContact(msn.ALLOW_LIST, userHandle)#.addCallback(addToBlockList)
+                               else:
+                                       addToBlockList()
+               
+               else: # The user wants to change subscription to the transport
+                       if(subtype == "subscribe"):
+                               updatePresence("subscribed")
+                       
+                       elif(subtype == "subscribed"):
+                               return # Nothing to do
+                       
+                       elif(subtype == "unsubscribe" or subtype == "unsubscribed"):
+                               # They want to unregister. Ok, we can do that
+                               jid = self.session.jabberID
+                               debug.log("Subscriptions: Session \"%s\" is about to be unregistered" % (jid))
+                               self.session.pytrans.registermanager.removeRegInfo(jid)
+                               debug.log("Subscriptions: Session \"%s\" has been unregistered" % (jid))
+       
diff --git a/src/main.py b/src/main.py
new file mode 100644 (file)
index 0000000..7010850
--- /dev/null
@@ -0,0 +1,325 @@
+# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Licensed for distribution under the GPL version 2, check COPYING for details
+
+import utils
+import os
+import shutil
+if(os.name == "posix"):
+       import signal
+import sys
+reload(sys)
+sys.setdefaultencoding("utf-8")
+import types
+
+# Must load config before everything else
+import config
+import xmlconfig
+xmlconfig.reloadConfig()
+
+if(config.reactor == "epoll"):
+       from twisted.internet import epollreactor
+       epollreactor.install()
+elif(config.reactor == "poll"):
+       from twisted.internet import pollreactor
+       pollreactor.install()
+elif(config.reactor == "kqueue"):
+       from twisted.internet import kqreactor
+       kqreactor.install()
+elif(len(config.reactor) > 0):
+       print "Unknown reactor: ", config.reactor, "Using default reactor"
+
+from twisted.internet import reactor, task
+from twisted.internet.defer import Deferred
+import twisted.python.log
+if(utils.checkTwisted()):
+       from twisted.words.protocols.jabber import component, jid
+       from twisted.xish.domish import Element
+else:
+       from tlib.jabber import component, jid
+       from tlib.domish import Element
+
+
+import xdb
+import session
+import jabw
+import disco
+import register
+import misciq
+import lang
+import debug
+import legacy
+
+#import gc
+#gc.set_debug(gc.DEBUG_COLLECTABLE | gc.DEBUG_UNCOLLECTABLE | gc.DEBUG_INSTANCES | gc.DEBUG_OBJECTS)
+
+
+class PyTransport(component.Service):
+       def __init__(self):
+               debug.log("PyTransport: Service starting up")
+               
+               # Discovery, as well as some builtin features
+               self.discovery = disco.ServerDiscovery(self)
+               self.discovery.addIdentity("gateway", legacy.id, legacy.name)
+               self.discovery.addIdentity("conference", "text", legacy.name + " Chatrooms")
+               self.discovery.addFeature("http://jabber.org/protocol/muc", None) # So that clients know you can create groupchat rooms on the server
+               
+               self.xdb = xdb.XDB(config.jid, legacy.mangle)
+               self.registermanager = register.RegisterManager(self)
+               self.gatewayTranslator = misciq.GatewayTranslator(self)
+               self.versionTeller = misciq.VersionTeller(self)
+               self.pingService = misciq.PingService(self)
+
+               self.xmlstream = None
+               self.sessions = {}
+               
+               # Groupchat ID handling
+               self.lastID = 0
+               self.reservedIDs = []
+
+               # Message IDs
+               self.messageID = 0
+               
+               self.loopCheckSessions = task.LoopingCall(self.loopCheckSessionsCall)
+               self.loopCheckSessions.start(60.0) # call every ten seconds
+               
+               # Display active sessions if debug mode is on
+               if(config.debugOn):
+                       self.loop = task.LoopingCall(self.loopCall)
+                       self.loop.start(60.0) # call every 60 seconds
+                       twisted.python.log.addObserver(self.exceptionLogger)
+               
+       
+       def removeMe(self):
+               debug.log("PyTransport: Service shutting down")
+               dic = utils.copyDict(self.sessions)
+               for session in dic:
+                       dic[session].removeMe()
+       
+       def exceptionLogger(self, *kwargs):
+               if(len(config.debugLog) > 0):
+                       kwargs = kwargs[0]
+                       if(kwargs.has_key("failure")):
+                               failure = kwargs["failure"]
+                               failure.printTraceback(debug) # Pass debug as a pretend file object because it implements the write method
+                               if(config.debugLog):
+                                       debug.flushDebugSmart()
+                                       print "Exception occured! Check the log!"
+       
+       def makeMessageID(self):
+               self.messageID += 1
+               return str(self.messageID)
+       
+       def makeID(self):
+               newID = "r" + str(self.lastID)
+               self.lastID += 1
+               if(self.reservedIDs.count(newID) > 0):
+                       # Ack, it's already used.. Try again
+                       return self.makeID()
+               else:
+                       return newID
+       
+       def reserveID(self, ID):
+               self.reservedIDs.append(ID)
+       
+       def loopCall(self):
+               if(len(self.sessions) > 0):
+                       debug.log("Sessions:")
+                       for key in self.sessions:
+                               debug.log("\t" + self.sessions[key].jabberID)
+       
+       def loopCheckSessionsCall(self):
+               if(len(self.sessions) > 0):
+                       oldDict = utils.copyDict(self.sessions)
+                       self.sessions = {}
+                       for key in oldDict:
+                               session = oldDict[key]
+                               if(not session.alive):
+                                       debug.log("Ghost session %s found. This shouldn't happen. Trace" % (session.jabberID))
+                                       # Don't add it to the new dictionary. Effectively removing it
+                               else:
+                                       self.sessions[key] = session
+       
+       def componentConnected(self, xmlstream):
+               debug.log("PyTransport: Connected to main Jabberd server")
+               self.xmlstream = xmlstream
+               self.xmlstream.addObserver("/iq", self.discovery.onIq)
+               self.xmlstream.addObserver("/presence", self.onPresence)
+               self.xmlstream.addObserver("/message", self.onMessage)
+               self.xmlstream.addObserver("/route", self.onRouteMessage)
+       
+       def componentDisconnected(self):
+               debug.log("PyTransport: Disconnected from main Jabberd server")
+               self.xmlstream = None
+       
+       def onRouteMessage(self, el):
+               for child in el.elements():
+                       if(child.name == "message"):
+                               self.onMessage(child)
+                       elif(child.name == "presence"):
+                               self.onPresence(child)
+                       elif(child.name == "iq"):
+                               self.discovery.onIq(child)
+       
+       def onMessage(self, el):
+               fro = el.getAttribute("from")
+               froj = jid.JID(fro)
+               to = el.getAttribute("to")
+#              if(to.find('@') < 0): return
+               mtype = el.getAttribute("type")
+               ulang = utils.getLang(el)
+               body = None
+               for child in el.elements():
+                       if(child.name == "body"):
+                               body = child.__str__()
+               if(self.sessions.has_key(froj.userhost())):
+                       self.sessions[froj.userhost()].onMessage(el)
+               elif(mtype != "error"):
+                       debug.log("PyTrans: Sending error response to a message outside of session.")
+                       jabw.sendErrorMessage(self, fro, to, "auth", "not-authorized", lang.get(ulang).notLoggedIn, body)
+       
+       def onPresence(self, el):
+               fro = el.getAttribute("from")
+               ptype = el.getAttribute("type")
+               froj = jid.JID(fro)
+               to = el.getAttribute("to")
+               toj = jid.JID(to)
+               ulang = utils.getLang(el)
+               if(self.sessions.has_key(froj.userhost())):
+                       self.sessions[froj.userhost()].onPresence(el)
+               else:
+                       if(to.find('@') < 0):
+                               # If the presence packet is to the transport (not a user) and there isn't already a session
+                               if(el.getAttribute("type") in [None, ""]): # Don't create a session unless they're sending available presence
+                                       debug.log("PyTransport: Attempting to create a new session \"%s\"" % (froj.userhost()))
+                                       s = session.makeSession(self, froj.userhost(), ulang)
+                                       if(s):
+                                               self.sessions[froj.userhost()] = s
+                                               debug.log("PyTransport: New session created \"%s\"" % (froj.userhost()))
+                                               # Send the first presence
+                                               s.onPresence(el)
+                                       else:
+                                               debug.log("PyTransport: Failed to create session \"%s\"" % (froj.userhost()))
+                                               jabw.sendMessage(self, to=froj.userhost(), fro=config.jid, body=lang.get(ulang).notRegistered)
+                               
+                               elif(el.getAttribute("type") != "error"):
+                                       debug.log("PyTransport: Sending unavailable presence to non-logged in user \"%s\"" % (froj.userhost()))
+                                       pres = Element((None, "presence"))
+                                       pres.attributes["from"] = to
+                                       pres.attributes["to"] = fro
+                                       pres.attributes["type"] = "unavailable"
+                                       self.send(pres)
+                                       return
+                       
+                       elif(ptype in ["subscribe", "subscribed", "unsubscribe", "unsubscribed"]):
+                               # They haven't logged in, and are trying to change subscription to a user
+                               # Lets log them in and then do it
+                               debug.log("PyTransport: Attempting to create a session to do subscription stuff %s" % (froj.userhost()))
+                               s = session.makeSession(self, froj.userhost(), ulang)
+                               if(s):
+                                       self.sessions[froj.userhost()] = s
+                                       debug.log("PyTransport: New session created \"%s\"" % (froj.userhost()))
+                                       # Tell the session there's a new resource
+                                       s.handleResourcePresence(froj.userhost(), froj.resource, toj.userhost(), toj.resource, 0, None, None, None)
+                                       # Send this subscription
+                                       s.onPresence(el)
+
+
+class App:
+       def __init__(self):
+               # Check that there isn't already a PID file
+               if(os.path.isfile(utils.doPath(config.pid))):
+                       pf = open(utils.doPath(config.pid))
+                       pid = int(str(pf.readline().strip()))
+                       pf.close()
+                       if(os.name == "posix"):
+                               try:
+                                       os.kill(pid, signal.SIGHUP)
+                                       self.alreadyRunning()
+                               except OSError:
+                                       # The process is still up
+                                       pass
+                       else:
+                               self.alreadyRunning()
+               
+               # Create a PID file
+               pid = str(os.getpid())
+               pf = file(utils.doPath(config.pid), 'w')
+               pf.write("%s\n" % pid);
+               pf.close()
+       
+               self.c = component.buildServiceManager(config.jid, config.secret, "tcp:%s:%s" % (config.mainServer, config.port))
+               self.transportSvc = PyTransport()
+               self.transportSvc.setServiceParent(self.c)
+               self.c.startService()
+               reactor.addSystemEventTrigger('before', 'shutdown', self.shuttingDown)
+       
+       def alreadyRunning(self):
+               print "There is already a transport instance running with this configuration."
+               print "Exiting..."
+               sys.exit(1)
+       
+       def shuttingDown(self):
+               self.transportSvc.removeMe()
+               def cb(ignored=None):
+                       os.remove(utils.doPath(config.pid))
+               d = Deferred()
+               d.addCallback(cb)
+               reactor.callLater(3.0, d.callback, None)
+               return d
+
+
+
+def SIGHUPstuff(*args):
+       xmlconfig.reloadConfig()
+       debug.reopenFile()
+
+def doSpoolPrepCheck():
+       pre = utils.doPath(config.spooldir) + "/" + config.jid + "/"
+       try:
+               f = open(pre + "notes_to_myself", "r")
+               for line in f.readlines():
+                       if line == "doSpoolPrepCheck\n":
+                               return
+               f.close()
+       except IOError:
+               pass
+       
+       # New installation
+       if not os.path.exists(pre):
+               os.makedirs(pre)
+               f = open(pre + "notes_to_myself", "w")
+               f.write("doSpoolPrepCheck\n")
+               f.close()
+               return
+
+       print "Checking spool files and stringprepping any if necessary...",
+       for file in os.listdir(pre):
+               if(file == "notes_to_myself"): return
+               file = file.replace("%", "@")
+               filej = jid.JID(file).full()
+               if(file != filej):
+                       file = file.replace("@", "%")
+                       filej = filej.replace("@", "%")
+                       if(os.path.exists(filej)):
+                               print "Need to move", file, "to", filej, "but the latter exists!\nAborting!"
+                               os.exit(1)
+                       else:
+                               shutil.move(utils.doPath(pre + file, pre + filej))
+       print "done"
+       f = open(pre + "notes_to_myself", "a")
+       f.write("doSpoolPrepCheck\n")
+       f.close()
+
+
+if(__name__ == "__main__"):
+       # Set SIGHUP to reload the config file & close & open debug file
+       if(os.name == "posix"):
+               signal.signal(signal.SIGHUP, SIGHUPstuff)
+
+       # Check that all the spool files stringprepped
+       doSpoolPrepCheck()
+
+       app = App()
+       reactor.run()
+
+
diff --git a/src/misciq.py b/src/misciq.py
new file mode 100644 (file)
index 0000000..bbbde0f
--- /dev/null
@@ -0,0 +1,146 @@
+# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Licensed for distribution under the GPL version 2, check COPYING for details
+
+import utils
+if(utils.checkTwisted()):
+       from twisted.xish.domish import Element
+       from twisted.words.protocols.jabber import jid
+else:
+       from tlib.domish import Element
+       from tlib.jabber import jid
+from twisted.internet import reactor, task
+
+import legacy
+import config
+import debug
+import lang
+import sys
+
+
+class PingService:
+       def __init__(self, pytrans):
+               self.pytrans = pytrans
+               self.pingCounter = 0
+               self.pingCheckTask = task.LoopingCall(self.pingCheck)
+               reactor.callLater(10.0, self.start)
+       
+       def start(self):
+               self.pingCheckTask.start(120.0)
+       
+       def pingCheck(self):
+               if(self.pingCounter >= 2 and self.pytrans.xmlstream): # Two minutes of no response from the server
+                       self.pytrans.xmlstream.transport.loseConnection()
+               elif(config.mainServerJID):
+                       d = self.pytrans.discovery.sendIq(self.makePingPacket())
+                       d.addCallback(self.pongReceived)
+                       self.pingCounter += 1
+       
+       def pongReceived(self, el):
+               self.pingCounter = 0
+       
+       def makePingPacket(self):
+               iq = Element((None, "iq"))
+               iq.attributes["from"] = config.jid
+               iq.attributes["to"] = config.mainServerJID
+               iq.attributes["type"] = "get"
+               query = iq.addElement("query")
+               query.attributes["xmlns"] = "jabber:iq:version"
+               return iq
+
+class GatewayTranslator:
+       def __init__(self, pytrans):
+               self.pytrans = pytrans
+               self.pytrans.discovery.addFeature("jabber:iq:gateway", self.incomingIq)
+       
+       def incomingIq(self, el):
+               fro = el.getAttribute("from")
+               ID = el.getAttribute("id")
+               itype = el.getAttribute("type")
+               if(itype == "get"):
+                       self.sendPrompt(fro, ID, utils.getLang(el))
+               elif(itype == "set"):
+                       self.sendTranslation(fro, ID, el)
+       
+       
+       def sendPrompt(self, to, ID, ulang):
+               debug.log("GatewayTranslator: Sending translation details for jabber:iq:gateway - user %s %s" % (to, ID))
+               
+               iq = Element((None, "iq"))
+               
+               iq.attributes["type"] = "result"
+               iq.attributes["from"] = config.jid
+               iq.attributes["to"] = to
+               iq.attributes["id"] = ID
+               query = iq.addElement("query")
+               query.attributes["xmlns"] = "jabber:iq:gateway"
+               desc = query.addElement("desc")
+               desc.addContent(lang.get(ulang).gatewayTranslator)
+               prompt = query.addElement("prompt")
+               
+               self.pytrans.send(iq)
+       
+       def sendTranslation(self, to, ID, el):
+               debug.log("GatewayTranslator: Translating account for jabber:iq:gateway - user %s %s" % (to, ID))
+               
+               # Find the user's legacy account
+               legacyaccount = None
+               for query in el.elements():
+                       if(query.name == "query"):
+                               for child in query.elements():
+                                       if(child.name == "prompt"):
+                                               legacyaccount = str(child)
+                                               break
+                               break
+               
+               
+               if(legacyaccount and len(legacyaccount) > 0):
+                       debug.log("GatewayTranslator: Sending translated account for jabber:iq:gateway - user %s %s" % (to, ID))
+                       iq = Element((None, "iq"))
+                       iq.attributes["type"] = "result"
+                       iq.attributes["from"] = config.jid
+                       iq.attributes["to"] = to
+                       iq.attributes["id"] = ID
+                       query = iq.addElement("query")
+                       query.attributes["xmlns"] = "jabber:iq:gateway"
+                       prompt = query.addElement("prompt")
+                       prompt.addContent(legacy.translateAccount(legacyaccount))
+                       
+                       self.pytrans.send(iq)
+               
+               else:
+                       self.pytrans.discovery.sendIqNotValid(to, ID, "jabber:iq:gateway")
+
+
+
+class VersionTeller:
+       def __init__(self, pytrans):
+               self.pytrans = pytrans
+               self.pytrans.discovery.addFeature("jabber:iq:version", self.incomingIq)
+       
+       def incomingIq(self, el):
+               eltype = el.getAttribute("type")
+               if(eltype != "get"): return # Only answer "get" stanzas
+               
+               self.sendVersion(el)
+       
+       def sendVersion(self, el):
+               debug.log("Discovery: Sending transport version information")
+               iq = Element((None, "iq"))
+               iq.attributes["type"] = "result"
+               iq.attributes["from"] = config.jid
+               iq.attributes["to"] = el.getAttribute("from")
+               if(el.getAttribute("id")):
+                       iq.attributes["id"] = el.getAttribute("id")
+               query = iq.addElement("query")
+               query.attributes["xmlns"] = "jabber:iq:version"
+               name = query.addElement("name")
+               name.addContent(legacy.name)
+               version = query.addElement("version")
+               version.addContent(legacy.version)
+               os = query.addElement("os")
+               os.addContent("Python" + sys.version)
+               
+               self.pytrans.send(iq)
+
+
+
diff --git a/src/register.py b/src/register.py
new file mode 100644 (file)
index 0000000..880aef2
--- /dev/null
@@ -0,0 +1,192 @@
+# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Licensed for distribution under the GPL version 2, check COPYING for details
+
+import utils
+if(utils.checkTwisted()):
+       from twisted.xish.domish import Element
+       from twisted.words.protocols.jabber import jid
+else:
+       from tlib.domish import Element
+       from tlib.jabber import jid
+
+import session
+import config
+import debug
+import lang
+import jabw
+import legacy
+
+XMPP_STANZAS = 'urn:ietf:params:xml:ns:xmpp-stanzas'
+
+class RegisterManager:
+       def __init__(self, pytrans):
+               self.pytrans = pytrans
+               if config.allowRegister:
+                       self.pytrans.discovery.addFeature("jabber:iq:register", self.incomingRegisterIq)
+               debug.log("RegisterManager: Created")
+       
+       def removeRegInfo(self, jabberID):
+               debug.log("RegisterManager: removeRegInfo(\"%s\")" % (jabberID))
+               try:
+                       # If the session is active then send offline presences
+                       session = self.pytrans.sessions[jabberID]
+                       session.removeMe()
+               except KeyError:
+                       pass
+               
+               self.pytrans.xdb.remove(jabberID)
+               debug.log("RegisterManager: removeRegInfo(\"%s\") - done" % (jabberID))
+       
+       
+       def setRegInfo(self, jabberID, username, password, nickname):
+               debug.log("RegisterManager: setRegInfo(\"%s\", \"%s\", \"%s\", \"%s\")" % (jabberID, username, password, nickname))
+               if(len(password) == 0):
+                       (blah1, password, blah3) = self.getRegInfo(jabberID)
+               
+               reginfo = legacy.formRegEntry(username, password, nickname)
+               self.pytrans.xdb.set(jid.JID(jabberID).full(), legacy.namespace, reginfo)
+       
+       def getRegInfo(self, jabberID):
+               debug.log("RegisterManager: getRegInfo(\"%s\")" % (jabberID))
+               result = self.pytrans.xdb.request(jid.JID(jabberID).full(), legacy.namespace)
+               if(result == None):
+                       debug.log("RegisterManager: getRegInfo(\"%s\") - not registered!" % (jabberID))
+                       return None
+               
+               username, password, nickname = legacy.getAttributes(result)
+               
+               if(username and password and len(username) > 0 and len(password) > 0):
+                       debug.log("RegisterManager: getRegInfo(\"%s\") - returning reg info \"%s\" \"%s\" \"%s\"!" % (jabberID, username, password, utils.latin1(nickname)))
+                       return (username, password, nickname)
+               else:
+                       debug.log("RegisterManager: getRegInfo(\"%s\") - invalid registration data! %s %s %s" % (jabberID, username, password, utils.latin1(nickname)))
+                       return None
+       
+       def incomingRegisterIq(self, incoming):
+               # Check what type the Iq is..
+               itype = incoming.getAttribute("type")
+               debug.log("RegisterManager: In-band registration type \"%s\" received" % (itype))
+               if(itype == "get"):
+                       self.sendRegistrationFields(incoming)
+               elif(itype == "set"):
+                       self.updateRegistration(incoming)
+               
+       def sendRegistrationFields(self, incoming):
+               # Construct a reply with the fields they must fill out
+               debug.log("RegisterManager: sendRegistrationFields() for \"%s\" \"%s\"" % (incoming.getAttribute("from"), incoming.getAttribute("id")))
+               reply = Element((None, "iq"))
+               reply.attributes["from"] = config.jid
+               reply.attributes["to"] = incoming.getAttribute("from")
+               reply.attributes["id"] = incoming.getAttribute("id")
+               reply.attributes["type"] = "result"
+               query = reply.addElement("query")
+               query.attributes["xmlns"] = "jabber:iq:register"
+               instructions = query.addElement("instructions")
+               ulang = utils.getLang(incoming)
+               instructions.addContent(lang.get(ulang).registerText)
+               userEl = query.addElement("username")
+               passEl = query.addElement("password")
+               nickEl = query.addElement("nick")
+               
+               # Check to see if they're registered
+               barefrom = jid.JID(incoming.getAttribute("from")).userhost()
+               result = self.getRegInfo(barefrom)
+               if(result):
+                       username, password, nickname = result
+                       userEl.addContent(username)
+                       if(nickname and len(nickname) > 0):
+                               nickEl.addContent(nickname)
+                       query.addElement("registered")
+               
+               self.pytrans.send(reply)
+       
+       def updateRegistration(self, incoming):
+               # Grab the username, password and nickname
+               debug.log("RegisterManager: updateRegistration() for \"%s\" \"%s\"" % (incoming.getAttribute("from"), incoming.getAttribute("id")))
+               source = jid.JID(incoming.getAttribute("from")).userhost()
+               ulang = utils.getLang(incoming)
+               username = None
+               password = None
+               nickname = None
+               
+               for queryFind in incoming.elements():
+                       if(queryFind.name == "query"):
+                               for child in queryFind.elements():
+                                       try:
+                                               if(child.name == "username"):
+                                                       username = child.__str__()
+                                               elif(child.name == "password"):
+                                                       password = child.__str__()
+                                               elif(child.name == "nick"):
+                                                       nickname = child.__str__()
+                                               elif(child.name == "remove"):
+                                                       # The user wants to unregister the transport! Gasp!
+                                                       debug.log("RegisterManager: Session \"%s\" is about to be unregistered" % (source))
+                                                       try:
+                                                               self.removeRegInfo(source)
+                                                               self.successReply(incoming)
+                                                       except:
+                                                               self.xdbErrorReply(incoming)
+                                                               return
+                                                       debug.log("RegisterManager: Session \"%s\" has been unregistered" % (source))
+                                                       return
+                                       except AttributeError, TypeError:
+                                               continue # Ignore any errors, we'll check everything below
+               
+               if(username and password and len(username) > 0 and len(password) > 0):
+                       # Valid registration data
+                       debug.log("RegisterManager: Valid registration data was received. Attempting to update XDB")
+                       try:
+                               self.setRegInfo(source, username, password, nickname)
+                               debug.log("RegisterManager: Updated XDB successfully")
+                               self.successReply(incoming)
+                               debug.log("RegisterManager: Sent off a result Iq")
+                               # If they're in a session right now we update their nick, otherwise request their auth
+                               if(self.pytrans.sessions.has_key(source)):
+                                       s = self.pytrans.sessions[source]
+                                       s.updateNickname(nickname)
+                               else:
+                                       (user, host, res) = jid.parse(incoming.getAttribute("from"))
+                                       jabw.sendPresence(self.pytrans, to=user + "@" + host, fro=config.jid, ptype="subscribe")
+                               if(config.registerMessage):
+                                       jabw.sendMessage(self.pytrans, to=incoming.getAttribute("from"), fro=config.jid, body=config.registerMessage)
+                       except:
+                               self.xdbErrorReply(incoming)
+                               raise
+               
+               else:
+                       self.badRequestReply(incoming)
+       
+       def badRequestReply(self, incoming):
+               debug.log("RegisterManager: Invalid registration data was sent to us. Or the removal failed.")
+               # Send an error Iq
+               reply = incoming
+               reply.swapAttributeValues("to", "from")
+               reply.attributes["type"] = "error"
+               error = reply.addElement("error")
+               error.attributes["type"] = "modify"
+               interror = error.addElement("bad-request")
+               interror["xmlns"] = XMPP_STANZAS
+               self.pytrans.send(reply)
+       
+       def xdbErrorReply(self, incoming):
+               debug.log("RegisterManager: Failure in updating XDB or sending result Iq")
+               # send an error Iq
+               reply = incoming
+               reply.swapAttributeValues("to", "from")
+               reply.attributes["type"] = "error"
+               error = reply.addElement("error")
+               error.attributes["type"] = "wait"
+               interror = error.addElement("internal-server-error")
+               interror["xmlns"] = XMPP_STANZAS
+               self.pytrans.send(reply)
+       
+       def successReply(self, incoming):
+               reply = Element((None, "iq"))
+               reply.attributes["type"] = "result"
+               ID = incoming.getAttribute("id")
+               if(ID): reply.attributes["id"] = ID
+               reply.attributes["from"] = config.jid
+               reply.attributes["to"] = incoming.getAttribute("from")
+               self.pytrans.send(reply)
+
diff --git a/src/session.py b/src/session.py
new file mode 100644 (file)
index 0000000..59b672a
--- /dev/null
@@ -0,0 +1,266 @@
+# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Licensed for distribution under the GPL version 2, check COPYING for details
+
+import utils
+import legacy
+import jabw
+import debug
+import config
+import lang
+
+
+
+def makeSession(pytrans, jabberID, ulang):
+       """ Tries to create a session object for the corresponding JabberID. Retrieves information
+       from XDB to create the session. If it fails, then the user is most likely not registered with
+       the transport """
+       debug.log("session: makeSession(\"%s\")" % (jabberID))
+       if(pytrans.sessions.has_key(jabberID)):
+               debug.log("session: makeSession() - removing existing session")
+               pytrans.sessions[jabberID].removeMe()
+       result = pytrans.registermanager.getRegInfo(jabberID)
+       if(result):
+               username, password, nickname = result
+               return Session(pytrans, jabberID, username, password, nickname, ulang)
+       else:
+               return None
+
+
+
+class Session(jabw.JabberConnection):
+       """ A class to represent each registered user's session with the legacy network. Exists as long as there
+       is a Jabber resource for the user available """
+       
+       def __init__(self, pytrans, jabberID, username, password, nickname, ulang):
+               """ Initialises the session object and connects to the legacy network """
+               jabw.JabberConnection.__init__(self, pytrans, jabberID)
+               debug.log("Session: Creating new session \"%s\"" % (jabberID))
+               
+               self.pytrans = pytrans
+               self.alive = True
+               self.ready = False # Only ready when we're logged into the legacy service
+               self.jabberID = jabberID # the JabberID of the Session's user
+               self.username = username # the legacy network ID of the Session's user
+               self.password = password
+               self.nickname = nickname
+               self.lang = ulang
+               
+               self.show = None
+               self.status = None
+               
+               self.resourceList = {}
+               self.groupchats = []
+               
+               self.legacycon = legacy.LegacyConnection(self.username, self.password, self)
+               
+               if(config.sessionGreeting):
+                       self.sendMessage(to=self.jabberID, fro=config.jid, body=config.sessionGreeting)
+               debug.log("Session: New session created \"%s\" \"%s\" \"%s\" \"%s\"" % (jabberID, username, password, nickname))
+       
+       def removeMe(self):
+               """ Safely removes the session object, including sending <presence type="unavailable"/> messages for each legacy related item on the user's contact list """
+               # Send offline presence to Jabber ID
+               # Delete all objects cleanly
+               # Remove this Session object from the pytrans
+               
+               debug.log("Session: Removing \"%s\"" % (self.jabberID))
+               
+               # Mark as dead
+               self.alive = False
+               self.ready = False
+               
+               # Send offline presence to the user
+               if(self.pytrans):
+                       self.sendPresence(to=self.jabberID, fro=config.jid, ptype="unavailable")
+               
+               # Clean up stuff on the legacy service end (including sending offline presences for all contacts)
+               if(self.legacycon):
+                       self.legacycon.removeMe()
+                       self.legacycon = None
+               
+               # Remove any groupchats we may be in
+               for groupchat in utils.copyList(self.groupchats):
+                       groupchat.removeMe()
+               
+               if(self.pytrans):
+                       # Remove us from the session list
+                       del self.pytrans.sessions[self.jabberID]
+                       # Clean up the no longer needed reference
+                       self.pytrans = None
+               
+               debug.log("Session: Completed removal \"%s\"" % (self.jabberID))
+               utils.mutilateMe(self)
+       
+       def updateNickname(self, nickname):
+               self.nickname = nickname
+               self.setStatus(self.show, self.status)
+       
+       def setStatus(self, show, status):
+               self.show = show
+               self.status = status
+               self.legacycon.setStatus(show, status)
+       
+       def sendNotReadyError(self, source, resource, dest, body):
+               self.sendErrorMessage(source + '/' + resource, dest, "wait", "not-allowed", lang.get(self.lang).waitForLogin, body)
+       
+       def findGroupchat(self, to):
+               pos = to.find('@')
+               if(pos > 0):
+                       roomID = to[:pos]
+               else:
+                       roomID = to
+               
+               for groupchat in self.groupchats:
+                       if(groupchat.ID == roomID):
+                               return groupchat
+               
+               return None
+               
+       def messageReceived(self, source, resource, dest, destr, mtype, body, noerror):
+               if(dest == config.jid):
+                       if(body.lower().startswith("end")):
+                               debug.log("Session: Received 'end' request. Killing session %s" % (self.jabberID))
+                               self.removeMe()
+                       return
+               
+               if(not self.ready):
+                       self.sendNotReadyError(source, resource, dest, body)
+                       return
+               
+               # Sends the message to the legacy translator
+               groupchat = self.findGroupchat(dest)
+               if(groupchat):
+                       # It's for a groupchat
+                       if(destr and len(destr) > 0 and not noerror):
+                               self.sendErrorMessage(to=(source + "/" + resource), fro=dest, etype="cancel", condition="not-allowed", explanation=lang.get(self.lang).groupchatPrivateError, body=body)
+                       else:
+                               debug.log("Session: Message received for groupchat \"%s\" \"%s\"" % (self.jabberID, groupchat.ID))
+                               groupchat.sendMessage(body, noerror)
+               else:
+                       debug.log("Session: messageReceived(), passing onto legacycon.sendMessage()")
+                       self.legacycon.sendMessage(dest, resource, body, noerror)
+       
+       def inviteReceived(self, source, resource, dest, destr, roomjid):
+               if(not self.ready):
+                       self.sendNotReadyError(source, resource, dest, roomjid)
+                       return
+               
+               if(not roomjid.endswith('@' + config.jid)):
+                       message = lang.get(self.lang).groupchatAdvocacy % (self.jabberID, config.website)
+                       self.legacycon.sendMessage(dest, resource, message, True)
+                       return
+               
+               groupchat = self.findGroupchat(roomjid)
+               if(groupchat):
+                       debug.log("Session: inviteReceived(\"%s\", \"%s\", \"%s\", \"%s\", \"%s\")" % (source, resource, dest, destr, roomjid))
+                       groupchat.sendContactInvite(dest)
+       
+       def typingNotificationReceived(self, dest, resource, composing):
+               """ The user has sent typing notification to a contact on the legacy service """
+               self.legacycon.userTypingNotification(dest, resource, composing)
+       
+       def presenceReceived(self, source, resource, to, tor, priority, ptype, show, status):
+               # Checks resources and priorities so that the highest priority resource always appears as the
+               # legacy services status. If there are no more resources then the session is deleted
+               # Additionally checks if the presence is to a groupchat room
+               groupchat = self.findGroupchat(to)
+               if(groupchat):
+                       # It's for an existing groupchat
+                       if(ptype == "unavailable"):
+                               # Kill the groupchat
+                               debug.log("Session: Presence received to kill groupchat \"%s\" \"%s\"" % (self.jabberID, groupchat.ID))
+                               groupchat.removeMe()
+                       else:
+                               if(source == self.jabberID):
+                                       debug.log("Session: Presence for groupchat \"%s\" \"%s\"" % (self.jabberID, groupchat.ID))
+                                       if(ptype == "error"):
+                                               groupchat.removeMe()
+                                       else:
+                                               groupchat.userJoined(tor)
+                               else:
+                                       debug.log("Session: Sending error presence for groupchat (user not allowed) \"%s\" \"%s\"" % (self.jabberID, groupchat.ID))
+                                       self.sendPresence(to=(source + "/" + resource), fro=to, ptype="error")
+               
+               elif(legacy.isGroupJID(to) and to != config.jid and ptype not in ["error", "unavailable"]):
+                       if(not self.ready):
+                               self.sendNotReadyError(source, resource, to, to)
+                               return
+                       # It's a new groupchat
+                       gcID = to[:to.find('@')] # Grab the room name
+                       debug.log("Session: Creating a new groupchat \"%s\" \"%s\"" % (self.jabberID, gcID))
+                       groupchat = legacy.LegacyGroupchat(self, resource, gcID) # Creates an empty groupchat
+                       groupchat.userJoined(tor)
+               
+               else:
+                       # Not for groupchat
+                       self.handleResourcePresence(source, resource, to, tor, priority, ptype, show, status)
+
+               
+       def handleResourcePresence(self, source, resource, to, tor, priority, ptype, show, status):
+               if(not ptype in [None, "unavailable"]): return # Ignore presence errors, probes, etc
+               if(to.find('@') > 0): return # Ignore presence packets sent to users
+               
+               existing = self.resourceList.has_key(resource)
+               if(ptype == "unavailable"):
+                       if(existing):
+                               debug.log("Session: %s - resource \"%s\" gone offline" % (self.jabberID, resource))
+                               self.resourceOffline(resource)
+                       else:
+                               return # I don't know the resource, and they're leaving, so it's all good
+               else:
+                       if(not existing):
+                               debug.log("Session %s - resource \"%s\" has come online" % (self.jabberID, resource))
+                               self.legacycon.newResourceOnline(resource)
+                       debug.log("Session %s - resource \"%s\" setting \"%s\" \"%s\" \"%s\"" % (self.jabberID, resource, show, status, priority)) 
+                       self.resourceList[resource] = SessionResource(show, status, priority)
+
+               highestActive = self.highestResource()
+
+               if(highestActive):
+                       # If we're the highest active resource, we should update the legacy service
+                       debug.log("Session %s - updating status on legacy service, resource %s" % (self.jabberID, highestActive))
+                       r = self.resourceList[highestActive]
+                       self.setStatus(r.show, r.status)
+               else:
+                       debug.log("Session %s - tearing down, last resource gone offline")
+                       self.removeMe()
+       
+       def highestResource(self):
+               """ Returns the highest priority resource """
+               highestActive = None
+               for checkR in self.resourceList.keys():
+                       if(highestActive == None or self.resourceList[checkR].priority > self.resourceList[highestActive].priority): 
+                               highestActive = checkR
+               
+               if(highestActive):
+                       debug.log("Session %s - highest active resource is \"%s\" at %d" % (self.jabberID, highestActive, self.resourceList[highestActive].priority))
+
+               return highestActive
+               
+       
+       def resourceOffline(self, resource):
+               del self.resourceList[resource]
+               self.legacycon.resourceOffline(resource)
+       
+       def subscriptionReceived(self, to, subtype):
+               """ Sends the subscription request to the legacy services handler """
+               debug.log("Session: \"%s\" subscriptionReceived(), passing onto legacycon.jabberSubscriptionReceived()" % (self.jabberID))
+               self.legacycon.jabberSubscriptionReceived(to, subtype)
+       
+
+
+
+
+
+class SessionResource:
+       """ A convienence class to allow comparisons of Jabber resources """
+       def __init__(self, show=None, status=None, priority=None):
+               self.show = show
+               self.status = status
+               self.priority = 0
+               try:
+                       self.priority = int(priority) 
+               except TypeError: pass
+               except ValueError: pass
+
+
diff --git a/src/tlib/__init__.py b/src/tlib/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/tlib/domish.py b/src/tlib/domish.py
new file mode 100644 (file)
index 0000000..60f4aa1
--- /dev/null
@@ -0,0 +1,723 @@
+# -*- test-case-name: twisted.test.test_domish -*-
+#
+# Twisted, the Framework of Your Internet
+# Copyright (C) 2001 Matthew W. Lefkowitz
+# 
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of version 2.1 of the GNU Lesser General Public
+# License as published by the Free Software Foundation.
+# 
+# This library 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
+# Lesser General Public License for more details.
+# 
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from __future__ import generators
+
+import types
+
+try:
+    import cStringIO as StringIO
+except ImportError:
+    import StringIO
+
+def _splitPrefix(name):
+    """Internal method for splitting a prefixed Element name into its respective parts """
+    ntok = name.split(":", 1)
+    if len(ntok) == 2:
+        return ntok
+    else:
+        return (None, ntok[0])
+
+class _Serializer:
+    """ Internal class which serializes an Element tree into a buffer """
+    def __init__(self, prefixes = None):
+        self.cio = StringIO.StringIO()
+        self.prefixes = prefixes or {}
+        self.prefixCounter = 0
+
+    def getValue(self):
+        return self.cio.getvalue()
+
+    def getPrefix(self, uri):
+        if not self.prefixes.has_key(uri):
+            self.prefixes[uri] = "xn%d" % (self.prefixCounter)
+            self.prefixCounter = self.prefixCounter + 1
+        return self.prefixes[uri]
+
+    def serialize(self, elem, closeElement = 1):
+        # Optimization shortcuts
+        write = self.cio.write
+
+        # Shortcut, check to see if elem is actually a chunk o' serialized XML
+        if isinstance(elem, SerializedXML):
+            write(elem.encode("utf-8"))
+            return
+
+        # Shortcut, check to see if elem is actually a string (aka Cdata)
+        if isinstance(elem, types.StringTypes):
+            write(escapeToXml(elem).encode("utf-8"))
+            return
+
+        # Further optimizations
+        parent = elem.parent
+        name = elem.name
+        uri = elem.uri
+        defaultUri = elem.defaultUri
+
+        
+        # Seralize element name
+        if defaultUri == uri:
+            if parent == None or defaultUri == parent.defaultUri:
+                write("<%s" % (name))
+            else:
+                write("<%s xmlns='%s' " % (name, defaultUri))
+        else:
+            prefix = self.getPrefix(uri)
+            if parent == None or elem.defaultUri == parent.defaultUri:
+                write("<%s:%s xmlns:%s='%s'" % (prefix, name, prefix, uri))
+            else:
+               write("<%s:%s xmlns:%s='%s' xmlns='%s'" % (prefix, name, prefix, uri, defaultUri))
+
+        # Serialize attributes
+        for k,v in elem.attributes.items():
+            # If the attribute name is a list, it's a qualified attribute
+            if isinstance(k, types.TupleType):
+                write(" %s:%s='%s'" % (self.getPrefix(k[0]), k[1], escapeToXml(v, 1)).encode("utf-8"))
+            else:
+                write((" %s='%s'" % ( k, escapeToXml(v, 1))).encode("utf-8"))
+
+        # Shortcut out if this is only going to return
+        # the element (i.e. no children)
+        if closeElement == 0:
+            write(">")
+            return
+
+        # Serialize children
+        if len(elem.children) > 0:
+            write(">")
+            for c in elem.children:
+                self.serialize(c)
+            # Add closing tag
+            if defaultUri == uri:
+                write("</%s>" % (name))
+            else:
+                write("</%s:%s>" % (self.getPrefix(uri), name))
+        else:
+            write("/>")
+
+class _ListSerializer:
+    """ Internal class which serializes an Element tree into a buffer """
+    def __init__(self, prefixes = None):
+        self.writelist = []
+        self.prefixes = prefixes or {}
+        self.prefixCounter = 0
+
+    def getValue(self):
+        d = "".join(self.writelist)
+        return d.encode("utf-8")
+
+    def getPrefix(self, uri):
+        if not self.prefixes.has_key(uri):
+            self.prefixes[uri] = "xn%d" % (self.prefixCounter)
+            self.prefixCounter = self.prefixCounter + 1
+        return self.prefixes[uri]
+
+    def serialize(self, elem, closeElement = 1):
+        # Optimization shortcuts
+        write = self.writelist.append
+
+        # Shortcut, check to see if elem is actually a chunk o' serialized XML
+        if isinstance(elem, SerializedXML):
+            write(elem)
+            return
+
+        # Shortcut, check to see if elem is actually a string (aka Cdata)
+        if isinstance(elem, types.StringTypes):
+            write(escapeToXml(elem))
+            return
+
+        # Further optimizations
+        parent = elem.parent
+        name = elem.name
+        uri = elem.uri
+        defaultUri = elem.defaultUri
+        
+        # Seralize element name
+        if defaultUri == uri:
+            if parent == None or defaultUri == parent.defaultUri:
+                write("<%s" % (name))
+            else:
+                write("<%s xmlns='%s' " % (name, defaultUri))
+        else:
+            prefix = self.getPrefix(uri)
+            if parent == None or elem.defaultUri == parent.defaultUri:
+                write("<%s:%s xmlns:%s='%s'" % (prefix, name, prefix, uri))
+            else:
+               write("<%s:%s xmlns:%s='%s' xmlns='%s'" % (prefix, name, prefix, uri, defaultUri))
+
+        # Serialize attributes
+        for k,v in elem.attributes.items():
+            # If the attribute name is a list, it's a qualified attribute
+            if isinstance(k, types.TupleType):
+                write(" %s:%s='%s'" % (self.getPrefix(k[0]), k[1], escapeToXml(v, 1)))
+            else:
+                write((" %s='%s'" % ( k, escapeToXml(v, 1))))
+
+        # Shortcut out if this is only going to return
+        # the element (i.e. no children)
+        if closeElement == 0:
+            write(">")
+            return
+
+        # Serialize children
+        if len(elem.children) > 0:
+            write(">")
+            for c in elem.children:
+                self.serialize(c)
+            # Add closing tag
+            if defaultUri == uri:
+                write("</%s>" % (name))
+            else:
+                write("</%s:%s>" % (self.getPrefix(uri), name))
+        else:
+            write("/>")
+
+
+SerializerClass = _Serializer
+
+def escapeToXml(text, isattrib = 0):
+    """Escape text to proper XML form, per section 2.3 in the XML specification.
+
+     @type text: L{str}
+     @param text: Text to escape
+
+     @type isattrib: L{Boolean}
+     @param isattrib: Triggers escaping of characters necessary for use as attribute values
+    """
+    text = text.replace("&", "&amp;")
+    text = text.replace("<", "&lt;")
+    text = text.replace(">", "&gt;")
+    if isattrib == 1:
+        text = text.replace("'", "&apos;")
+        text = text.replace("\"", "&quot;")
+    return text
+
+def unescapeFromXml(text):
+    text = text.replace("&lt;", "<")
+    text = text.replace("&gt;", ">")
+    text = text.replace("&apos;", "'")
+    text = text.replace("&quot;", "\"")
+    text = text.replace("&amp;", "&")
+    return text
+
+def generateOnlyKlass(list, klass):
+    """ Filters items in a list by class
+    """
+    for n in list:
+        if n.__class__ == klass:
+            yield n
+
+def generateElementsQNamed(list, name, uri):
+    """ Filters Element items in a list with matching name and URI
+    """
+    for n in list:
+        if n.__class__ == Element and n.name == name and n.uri == uri:
+            yield n
+
+def generateElementsNamed(list, name):
+    """ Filters Element items in a list with matching name, regardless of URI
+    """
+    for n in list:
+        if n.__class__ == Element and n.name == name:
+            yield n
+
+
+class SerializedXML(str):
+    """ Marker class for pre-serialized XML in the DOM """
+    pass
+
+        
+class Namespace:
+    """ Convenience object for tracking namespace declarations
+    """
+    def __init__(self, uri):
+        self._uri = uri
+    def __getattr__(self, n):
+        return (self._uri, n)
+    def __getitem__(self, n):
+        return (self._uri, n)
+
+
+class Element(object):
+    """Object representing a container (a.k.a. tag or element) in an HTML or XML document.
+
+    An Element contains a series of attributes (name/value pairs),
+    content (character data), and other child Element objects. When building a document
+    with markup (such as HTML or XML), use this object as the starting point.
+
+    @type uri: C{str}
+    @ivar uri: URI of this Element's name
+
+    @type defaultUri: C{str}
+    @ivar defaultUri: URI this Element exists within
+
+    @type name: C{str}
+    @ivar name: Name of this Element
+
+    @type children: C{list}
+    @ivar children: List of child Elements and content
+
+    @type parent: C{Element}
+    @ivar parent: Reference to the parent Element, if any.
+
+    @type attributes: C{dict}
+    @ivar attributes: Dictionary of attributes associated with this Element.
+
+    """
+    _idCounter = 0
+    def __init__(self, qname, defaultUri = None, attribs = None):
+        """
+        @param qname: Tuple of (uri, name)
+        @param defaultUri: The default URI of the element; defaults to the URI specified in L{qname}
+        @param attribs: Dictionary of attributes
+        """
+        self.uri, self.name = qname
+        self.defaultUri = defaultUri or self.uri
+        self.attributes = attribs or {}
+        self.children = []
+        self.parent = None
+
+    def __getattr__(self, key):
+        # Check child list for first Element with a name matching the key
+        for n in self.children:
+            if n.__class__ == Element and n.name == key:
+                return n
+            
+        # Tweak the behaviour so that it's more friendly about not
+        # finding elements -- we need to document this somewhere :)
+        return None
+            
+    def __getitem__(self, key):
+        return self.attributes[self._dqa(key)]
+
+    def __delitem__(self, key):
+        del self.attributes[self._dqa(key)];
+
+    def __setitem__(self, key, value):
+        self.attributes[self._dqa(key)] = value
+
+    def __str__(self):
+        """ Retrieve the first CData (content) node 
+        """
+        for n in self.children:
+            if isinstance(n, types.StringTypes): return n
+        return ""
+
+    def _dqa(self, attr):
+        """Dequalify an attribute key as needed"""
+        if isinstance(attr, types.TupleType) and attr[0] == self.uri:
+            return attr[1]
+        else:
+            return attr
+
+    def getAttribute(self, attribname, default = None):
+        """Retrieve the value of attribname, if it exists """
+        return self.attributes.get(attribname, default)
+
+    def hasAttribute(self, attrib):
+        """Determine if the specified attribute exists """
+        return self.attributes.has_key(self._dqa(attrib))
+    
+    def compareAttribute(self, attrib, value):
+        """Safely compare the value of an attribute against a provided value; None-safe. """
+        return self.attributes.get(self._dqa(attrib), None) == value
+
+    def swapAttributeValues(self, left, right):
+        """Swap the values of two attribute"""
+        d = self.attributes
+        l = d[left]
+        d[left] = d[right]
+        d[right] = l
+
+    def addChild(self, node):
+        """Add a child to this Element"""
+        if node.__class__ == Element:
+            node.parent = self
+        self.children.append(node)
+        return self.children[-1]
+
+    def addContent(self, text):
+        """Add some text data to this element"""
+        c = self.children
+        if len(c) > 0 and isinstance(c[-1], types.StringTypes):
+            c[-1] = c[-1] + text
+        else:
+            c.append(text)
+        return c[-1]
+
+    def addElement(self, name, defaultUri = None, content = None):
+        """Add a new child Element to this Element; preferred method
+        """
+        result = None
+        if isinstance(name, type(())):
+            defaultUri = defaultUri or name[0]
+            self.children.append(Element(name, defaultUri))
+        else:
+            defaultUri = defaultUri or self.defaultUri
+            self.children.append(Element((self.uri, name), defaultUri))
+
+        result = self.children[-1]
+        result.parent = self
+
+        if content:
+            result.children.append(content)
+
+        return result
+
+    def addRawXml(self, rawxmlstring):
+        """Add a pre-serialized chunk o' XML as a child of this Element.
+        """
+        self.children.append(SerializedXML(rawxmlstring))
+
+    def addUniqueId(self):
+        """Add a unique (across a given Python session) id attribute to this Element"""
+        self.attributes["id"] = "H_%d" % Element._idCounter
+        Element._idCounter = Element._idCounter + 1
+
+    def elements(self):
+        """Iterate across all children of this Element that are Elements"""
+        return generateOnlyKlass(self.children, Element)
+
+    def toXml(self, prefixes = None, closeElement = 1):
+        """Serialize this Element and all children to a string """
+        s = SerializerClass(prefixes)
+        s.serialize(self, closeElement)
+        return s.getValue()
+
+    def firstChildElement(self):
+        for c in self.children:
+            if c.__class__ == Element:
+                return c
+        return None
+
+    def getElement(self, tagName):
+        for child in self.elements():
+            if(child.name == tagName):
+                return child
+
+
+class ParserError(Exception):
+    """ Exception thrown when a parsing error occurs """
+    pass
+
+def elementStream():
+    """ Preferred method to construct an ElementStream
+
+    Uses Expat-based stream if available, and falls back to Sux if necessary.
+    """
+    try:
+        es = ExpatElementStream()
+        return es
+    except ImportError:
+        es = SuxElementStream()
+        return es
+
+from twisted.protocols import sux
+class SuxElementStream(sux.XMLParser):
+    def __init__(self):
+        self.connectionMade()
+        self.DocumentStartEvent = None
+        self.ElementEvent = None
+        self.DocumentEndEvent = None
+        self.currElem = None
+        self.rootElem = None
+        self.documentStarted = False
+        self.defaultNsStack = []
+        self.prefixStack = []
+        self.parse = self.dataReceived
+
+    def findUri(self, prefix):
+        # Walk prefix stack backwards, looking for the uri
+        # matching the specified prefix
+        stack = self.prefixStack
+        for i in range(-1, (len(self.prefixStack)+1) * -1, -1):
+            if prefix in stack[i]:
+                return stack[i][prefix]
+        return None
+
+    def gotTagStart(self, name, attributes):
+        defaultUri = None
+        localPrefixes = {}
+        attribs = {}
+        uri = None
+                    
+        # Pass 1 - Identify namespace decls
+        for k, v in attributes.items():
+            if k.startswith("xmlns"):
+                x, p = _splitPrefix(k)
+                if (x == None): # I.e.  default declaration
+                    defaultUri = v
+                else:
+                    localPrefixes[p] = v
+                del attributes[k]
+
+        # Push namespace decls onto prefix stack
+        self.prefixStack.append(localPrefixes)
+
+        # Determine default namespace for this element; if there
+        # is one
+        if defaultUri == None and len(self.defaultNsStack) > 0:
+            defaultUri = self.defaultNsStack[-1]
+                
+        # Fix up name
+        prefix, name = _splitPrefix(name)
+        if prefix == None: # This element is in the default namespace
+            uri = defaultUri
+        else:
+            # Find the URI for the prefix
+            uri = self.findUri(prefix)
+        
+        # Pass 2 - Fix up and escape attributes
+        for k, v in attributes.items():
+            p, n = _splitPrefix(k)
+            if p == None:
+                attribs[n] = v
+            else:
+                attribs[(self.findUri(p)), n] = unescapeFromXml(v)
+
+        # Construct the actual Element object
+        e = Element((uri, name), defaultUri, attribs)
+
+        # Save current default namespace
+        self.defaultNsStack.append(defaultUri)
+
+        # Document already started
+        if self.documentStarted:
+            # Starting a new packet
+            if self.currElem == None:
+                self.currElem = e
+            # Adding to existing element
+            else:
+                self.currElem = self.currElem.addChild(e)
+        # New document
+        else:
+            self.rootElem = e
+            self.documentStarted = True
+            self.DocumentStartEvent(e)
+
+    def gotText(self, data):
+        if self.currElem != None:
+            self.currElem.addContent(data)
+
+    def gotCData(self, data):
+        if self.currElem != None:
+            self.currElem.addContent(data)
+
+    def gotComment(self, data):
+        # Ignore comments for the moment
+        pass
+
+    entities = { "amp" : "&",
+                 "lt"  : "<",
+                 "gt"  : ">",
+                 "apos": "'",
+                 "quot": "\"" }
+
+    def gotEntityReference(self, entityRef):
+        # If this is an entity we know about, add it as content
+        # to the current element
+        if entityRef in SuxElementStream.entities:
+            self.currElem.addContent(SuxElementStream.entities[entityRef])
+
+    def gotTagEnd(self, name):
+        # Ensure the document hasn't already ended
+        if self.rootElem == None:
+            # XXX: Write more legible explanation
+            raise ParserError, "Element closed after end of document."
+        
+        # Fix up name
+        prefix, name = _splitPrefix(name)
+        if prefix == None:
+            uri = self.defaultNsStack[-1]
+        else:
+            uri = self.findUri(prefix)
+
+        # End of document
+        if self.currElem == None:
+            # Ensure element name and uri matches
+            if self.rootElem.name != name or self.rootElem.uri != uri:
+                raise ParserError, "Mismatched root elements"
+            self.DocumentEndEvent()
+            self.rootElem = None
+
+        # Other elements
+        else:
+            # Ensure the tag being closed matches the name of the current
+            # element
+            if self.currElem.name != name or self.currElem.uri != uri:
+                # XXX: Write more legible explanation
+                raise ParserError, "Malformed element close"
+
+            # Pop prefix and default NS stack
+            self.prefixStack.pop()
+            self.defaultNsStack.pop()
+
+            # Check for parent null parent of current elem;
+            # that's the top of the stack
+            if self.currElem.parent == None:
+                self.ElementEvent(self.currElem)
+                self.currElem = None
+
+            # Anything else is just some element wrapping up
+            else:
+                self.currElem = self.currElem.parent
+
+
+class ExpatElementStream:
+    def __init__(self):
+        import pyexpat
+        self.DocumentStartEvent = None
+        self.ElementEvent = None
+        self.DocumentEndEvent = None
+        self.parser = pyexpat.ParserCreate("UTF-8", " ")
+        self.parser.StartElementHandler = self._onStartElement
+        self.parser.EndElementHandler = self._onEndElement
+        self.parser.CharacterDataHandler = self._onCdata
+        self.parser.StartNamespaceDeclHandler = self._onStartNamespace
+        self.parser.EndNamespaceDeclHandler = self._onEndNamespace
+        self.currElem = None
+        self.defaultNsStack = []
+        self.documentStarted = 0        
+
+    def parse(self, buffer):
+        self.parser.Parse(buffer)
+
+    def _onStartElement(self, name, attrs):
+        # Generate a qname tuple from the provided name
+        qname = name.split(" ")
+
+        # Process attributes
+        for k, v in attrs.items():
+            if k.find(" ") != -1:
+#                attrs[k.split(" ")] = v
+                aqname = k.split(" ")
+                attrs[(aqname[0], aqname[1])] = v
+                del attrs[k]
+
+        # Construct the new element
+        e = Element(qname, self.defaultNsStack[-1], attrs)
+
+        # Document already started
+        if self.documentStarted == 1:
+            if self.currElem != None:
+                self.currElem.children.append(e)
+                e.parent = self.currElem
+            self.currElem = e
+
+        # New document
+        else:
+            self.documentStarted = 1
+            self.DocumentStartEvent(e)
+
+    def _onEndElement(self, _):
+        # Check for null current elem; end of doc
+        if self.currElem == None:
+            self.DocumentEndEvent()
+            
+        # Check for parent that is None; that's
+        # the top of the stack
+        elif self.currElem.parent == None:
+            self.ElementEvent(self.currElem)
+            self.currElem = None
+
+        # Anything else is just some element in the current
+        # packet wrapping up
+        else:
+            self.currElem = self.currElem.parent
+
+    def _onCdata(self, data):
+        if self.currElem != None:
+            self.currElem.addContent(data)
+
+    def _onStartNamespace(self, prefix, uri):
+        # If this is the default namespace, put
+        # it on the stack
+        if prefix == None:
+            self.defaultNsStack.append(uri)
+
+    def _onEndNamespace(self, prefix):
+        # Remove last element on the stack
+        if prefix == None:
+            self.defaultNsStack.pop()
+
+
+
+def parseText(text):
+    t = TextParser()
+    t.parseString(text)
+    return t.root
+
+def parseFile(filename):
+    t = TextParser()
+    t.parseFile(filename)
+    return t.root
+
+class TextParser:
+    """ Taken from http://xoomer.virgilio.it/dialtone/rsschannel.py """
+
+    def __init__(self):
+        self.root = None
+
+    def parseFile(self, filename):
+        return self.parseString(file(filename).read())
+
+    def parseString(self, data):
+        es = SuxElementStream()
+        es.DocumentStartEvent = self.docStart
+        es.DocumentEndEvent = self.docEnd
+        es.ElementEvent = self.element
+        es.parse(data)
+        return self.root
+
+    def docStart(self, e):
+        self.root = e
+
+    def docEnd(self):
+        pass
+
+    def element(self, e):
+        self.root.addChild(e)
+
+
+## class FileParser(ElementStream):
+##     def __init__(self):
+##         ElementStream.__init__(self)
+##         self.DocumentStartEvent = self.docStart
+##         self.ElementEvent = self.elem
+##         self.DocumentEndEvent = self.docEnd
+##         self.done = 0
+
+##     def docStart(self, elem):
+##         self.document = elem
+
+##     def elem(self, elem):
+##         self.document.addChild(elem)
+
+##     def docEnd(self):
+##         self.done = 1
+
+##     def parse(self, filename):
+##         for l in open(filename).readlines():
+##             self.parser.Parse(l)
+##         assert self.done == 1
+##         return self.document
+
+## def parseFile(filename):
+##     return FileParser().parse(filename)
+
+
diff --git a/src/tlib/jabber/__init__.py b/src/tlib/jabber/__init__.py
new file mode 100644 (file)
index 0000000..d22eb42
--- /dev/null
@@ -0,0 +1,22 @@
+
+# Twisted, the Framework of Your Internet
+# Copyright (C) 2001 Matthew W. Lefkowitz
+# 
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of version 2.1 of the GNU Lesser General Public
+# License as published by the Free Software Foundation.
+# 
+# This library 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
+# Lesser General Public License for more details.
+# 
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+"""
+
+Twisted Jabber: Jabber Protocol Helpers
+
+"""
diff --git a/src/tlib/jabber/client.py b/src/tlib/jabber/client.py
new file mode 100644 (file)
index 0000000..fb48782
--- /dev/null
@@ -0,0 +1,176 @@
+# -*- test-case-name: twisted.test.test_jabbercomponent -*-
+#
+# Twisted, the Framework of Your Internet
+# Copyright (C) 2001 Matthew W. Lefkowitz
+# 
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of version 2.1 of the GNU Lesser General Public
+# License as published by the Free Software Foundation.
+# 
+# This library 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
+# Lesser General Public License for more details.
+# 
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from tlib import domish
+from twisted.xish import xpath, utility
+from tlib import xmlstream
+
+DigestAuthQry = xpath.intern("/iq/query/digest")
+PlaintextAuthQry = xpath.intern("/iq/query/password")
+
+def basicClientFactory(jid, secret):
+    a = BasicAuthenticator(jid, secret)
+    return xmlstream.XmlStreamFactory(a)
+
+class IQ(domish.Element):
+    """ Wrapper for a Info/Query packet
+
+    This provides the necessary functionality to send IQs and get notified
+    when a result comes back. It's a subclass from domish.Element, so you can
+    use the standard DOM manipulation calls to add data to the outbound
+    request.
+
+    @type callbacks: C{hemp.utility.CallbackList}
+    @cvar callbacks: Callback list to be notified when response comes back
+    
+    """    
+    def __init__(self, xmlstream, type = "set"):
+        """
+        @type xmlstream: C{XmlStream}
+        @param xmlstream: XmlStream to use for transmission of this IQ
+
+        @type type: C{str}
+        @param type: IQ type identifier ('get' or 'set')
+
+        """
+        domish.Element.__init__(self, ("jabber:client", "iq"))
+        self.addUniqueId()
+        self["type"] = type
+        self._xmlstream = xmlstream
+        self.callbacks = utility.CallbackList()
+
+    def addCallback(self, fn, *args, **kwargs):
+        """
+        Register a callback for notification when the IQ result
+        is available.
+
+        """
+        self.callbacks.addCallback(True, fn, *args, **kwargs)
+
+    def send(self, to = None):
+        """
+        Call this method to send this IQ request via the associated XmlStream
+
+        @type to: C{str}
+        @type to: Jabber ID of the entity to send the request to
+
+        @returns: Callback list for this IQ. Any callbacks added to this list will
+                  be fired when the result comes back.
+        """
+        if to != None:
+            self["to"] = to
+        self._xmlstream.addOnetimeObserver("/iq[@id='%s']" % self["id"], \
+                                    self._resultEvent)
+        self._xmlstream.send(self.toXml())
+
+    def _resultEvent(self, iq):
+        self.callbacks.callback(iq)
+        self.callbacks = None
+
+class BasicAuthenticator(xmlstream.ConnectAuthenticator):
+    """ Authenticates an XmlStream against a Jabber server as a Client
+
+    This only implements non-SASL authentication, per
+    U{JEP 78<http://www.jabber.org/jeps/jep-0078.html>}. Additionally, this
+    authenticator provides the ability to perform inline registration, per
+    U{JEP 77<http://www.jabber.org/jeps/jep-0077.html>}.
+
+    Under normal circumstances, the BasicAuthenticator generates the L{STREAM_AUTHD_EVENT}
+    once the stream has authenticated. However, it can also generate other events, such
+    as:
+      - L{INVALID_USER_EVENT} : Authentication failed, due to invalid username
+      - L{AUTH_FAILED_EVENT} : Authentication failed, due to invalid password
+      - L{REGISTER_FAILED_EVENT} : Registration failed
+
+    If authentication fails for any reason, you can attempt to register by calling
+    the L{registerAccount} method. If the registration succeeds, a L{STREAM_AUTHD_EVENT}
+    will be fired. Otherwise, one of the above errors will be generated (again).
+    
+    """
+    namespace = "jabber:client"
+
+    INVALID_USER_EVENT    = "//event/client/basicauth/invaliduser"
+    AUTH_FAILED_EVENT     = "//event/client/basicauth/authfailed"
+    REGISTER_FAILED_EVENT = "//event/client/basicauth/registerfailed"
+
+    def __init__(self, jid, password):
+        xmlstream.ConnectAuthenticator.__init__(self, jid.host)
+        self.jid = jid
+        self.password = password
+
+    def streamStarted(self, rootelem):
+        # Send request for auth fields
+        iq = IQ(self.xmlstream, "get")
+        iq.addElement(("jabber:iq:auth", "query"))
+        iq.query.addElement("username", content = self.jid.user)
+        iq.addCallback(self._authQueryResultEvent)
+        iq.send()
+
+    def _authQueryResultEvent(self, iq):
+        if iq["type"] == "result":
+            # Construct auth request
+            iq = IQ(self.xmlstream, "set")
+            iq.addElement(("jabber:iq:auth", "query"))
+            iq.query.addElement("username", content = self.jid.user)
+            iq.query.addElement("resource", content = self.jid.resource)
+        
+            # Prefer digest over plaintext
+            if DigestAuthQry.matches(iq):
+                digest = xmlstream.hashPassword(self.xmlstream.sid, self.password)
+                iq.query.addElement("digest", content = digest)
+            else:
+                iq.query.addElement("password", content = self.password)
+
+            iq.addCallback(self._authResultEvent)
+            iq.send()
+        else:
+            # Check for 401 -- Invalid user
+            if iq.error["code"] == "401":
+                self.xmlstream.dispatch(iq, self.INVALID_USER_EVENT)
+            else:
+                self.xmlstream.dispatch(iq, self.AUTH_FAILED_EVENT)
+
+    def _authResultEvent(self, iq):
+        if iq["type"] == "result":
+            self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT)
+        else:
+            self.xmlstream.dispatch(iq, self.AUTH_FAILED_EVENT)
+
+    def registerAccount(self, username = None, password = None):
+        if username:
+            self.jid.user = username
+        if password:
+            self.password = password
+            
+        iq = IQ(self.xmlstream, "set")
+        iq.addElement(("jabber:iq:register", "query"))
+        iq.query.addElement("username", content = self.jid.user)
+        iq.query.addElement("password", content = self.password)
+
+        iq.addCallback(self._registerResultEvent)
+
+        iq.send()
+
+    def _registerResultEvent(self, iq):
+        if iq["type"] == "result":
+            # Registration succeeded -- go ahead and auth
+            self.streamStarted(None)
+        else:
+            # Registration failed
+            self.xmlstream.dispatch(iq, self.REGISTER_FAILED_EVENT)
+            
diff --git a/src/tlib/jabber/component.py b/src/tlib/jabber/component.py
new file mode 100644 (file)
index 0000000..6c7863b
--- /dev/null
@@ -0,0 +1,175 @@
+# -*- test-case-name: twisted.test.test_jabbercomponent -*-
+#
+# Twisted, the Framework of Your Internet
+# Copyright (C) 2001 Matthew W. Lefkowitz
+# 
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of version 2.1 of the GNU Lesser General Public
+# License as published by the Free Software Foundation.
+# 
+# This library 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
+# Lesser General Public License for more details.
+# 
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from tlib import domish
+from twisted.xish import xpath, utility
+from tlib import xmlstream
+from twisted.protocols.jabber import jstrports
+
+def componentFactory(componentid, password):
+    a = ConnectComponentAuthenticator(componentid, password)
+    return xmlstream.XmlStreamFactory(a)
+
+class ConnectComponentAuthenticator(xmlstream.ConnectAuthenticator):
+    """ Authenticator to permit an XmlStream to authenticate against a Jabber
+    Server as a Component (where the Authenticator is initiating the stream).
+
+    This implements the basic component authentication. Unfortunately this
+    protocol is not formally described anywhere. Fortunately, all the Jabber
+    servers I know of use this mechanism in exactly the same way.
+
+    """
+    namespace = 'jabber:component:accept'
+
+    def __init__(self, componentjid, password):
+        """
+        @type componentjid: C{str}
+        @param componentjid: Jabber ID that this component wishes to bind to.
+
+        @type password: C{str}
+        @param password: Password/secret this component uses to authenticate.
+        """
+        xmlstream.ConnectAuthenticator.__init__(self, componentjid)
+        self.password = password
+
+    def streamStarted(self, rootelem):
+        # Create handshake
+        hs = domish.Element(("jabber:component:accept", "handshake"))
+        hs.addContent(xmlstream.hashPassword(self.xmlstream.sid, self.password))
+
+        # Setup observer to watch for handshake result
+        self.xmlstream.addOnetimeObserver("/handshake", self._handshakeEvent)
+        self.xmlstream.send(hs)
+
+    def _handshakeEvent(self, elem):
+        self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT)
+
+class ListenComponentAuthenticator(xmlstream.Authenticator):
+    """ Placeholder for listening components """
+    pass
+
+
+from twisted.application import service
+from twisted.python import components
+
+class IService(components.Interface):
+    def componentConnected(self, xmlstream):
+        """ Parent component has established a connection
+        """
+
+    def componentDisconnected(self):
+        """ Parent component has lost a connection to the Jabber system
+        """
+
+    def transportConnected(self, xmlstream):
+        """ Parent component has established a connection over the underlying transport
+        """
+
+class Service(service.Service):
+    __implements__ = (IService, )
+
+    def componentConnected(self, xmlstream):
+        pass
+
+    def componentDisconnected(self):
+        pass
+
+    def transportConnected(self, xmlstream):
+        pass
+
+    def send(self, obj):
+        self.parent.send(obj)
+
+class ServiceManager(service.MultiService):
+    """ Business logic representing a managed component connection to a Jabber router
+
+    This Service maintains a single connection to a Jabber router and
+    provides facilities for packet routing and transmission. Business
+    logic modules can 
+    subclasses, and added as sub-service.
+    """
+    def __init__(self, jid, password):
+        service.MultiService.__init__(self)
+
+        # Setup defaults
+        self.jabberId = jid
+        self.xmlstream = None
+
+        # Internal buffer of packets
+        self._packetQueue = []
+
+        # Setup the xmlstream factory
+        self._xsFactory = componentFactory(self.jabberId, password)
+
+        # Register some lambda functions to keep the self.xmlstream var up to date
+        self._xsFactory.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT, self._connected)
+        self._xsFactory.addBootstrap(xmlstream.STREAM_AUTHD_EVENT, self._authd)
+        self._xsFactory.addBootstrap(xmlstream.STREAM_END_EVENT, self._disconnected)
+
+        # Map addBootstrap and removeBootstrap to the underlying factory -- is this
+        # right? I have no clue...but it'll work for now, until i can think about it
+        # more.
+        self.addBootstrap = self._xsFactory.addBootstrap
+        self.removeBootstrap = self._xsFactory.removeBootstrap
+
+    def getFactory(self):
+        return self._xsFactory
+
+    def _connected(self, xs):
+        self.xmlstream = xs
+        for c in self:
+            if components.implements(c, IService):
+                c.transportConnected(xs)
+
+    def _authd(self, xs):
+        # Flush all pending packets
+        for p in self._packetQueue:
+            self.xmlstream.send(p)
+        self._packetQueue = []
+
+        # Notify all child services which implement
+        # the IService interface
+        for c in self:
+            if components.implements(c, IService):
+                c.componentConnected(xs)
+
+    def _disconnected(self, _):
+        self.xmlstream = None
+
+        # Notify all child services which implement
+        # the IService interface
+        for c in self:
+            if components.implements(c, IService):
+                c.componentDisconnected()
+
+    def send(self, obj):
+        if self.xmlstream != None:
+            self.xmlstream.send(obj)
+        else:
+            self._packetQueue.append(obj)
+
+
+
+
+def buildServiceManager(jid, password, strport):
+    """ Constructs a pre-built C{component.ServiceManager}, using the specified strport string.    
+    """
+    svc = ServiceManager(jid, password)
+    client_svc = jstrports.client(strport, svc.getFactory())
+    client_svc.setServiceParent(svc)
+    return svc
diff --git a/src/tlib/jabber/jid.py b/src/tlib/jabber/jid.py
new file mode 100644 (file)
index 0000000..5e4d8df
--- /dev/null
@@ -0,0 +1,154 @@
+# -*- test-case-name: twisted.test.test_jabberjid -*-
+#
+# Twisted, the Framework of Your Internet
+# Copyright (C) 2001 Matthew W. Lefkowitz
+# 
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of version 2.1 of the GNU Lesser General Public
+# License as published by the Free Software Foundation.
+# 
+# This library 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
+# Lesser General Public License for more details.
+# 
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from twisted.internet import reactor, protocol, defer
+from tlib import domish
+from twisted.xish import utility
+from tlib.jabber.xmpp_stringprep import nodeprep, resourceprep, nameprep
+import string
+
+class InvalidFormat(Exception):
+    pass
+
+def parse(jidstring):
+    user = None
+    server = None
+    resource = None
+
+    # Search for delimiters
+    user_sep = jidstring.find("@")
+    res_sep  = jidstring.find("/")
+
+    if user_sep == -1:        
+        if res_sep == -1:
+            # host
+            server = jidstring
+        else:
+            # host/resource
+            server = jidstring[0:res_sep]
+            resource = jidstring[res_sep + 1:] or None
+    else:
+        if res_sep == -1:
+            # user@host
+            user = jidstring[0:user_sep] or None
+            server = jidstring[user_sep + 1:]
+        else:
+            if user_sep < res_sep:
+                # user@host/resource
+                user = jidstring[0:user_sep] or None
+                server = jidstring[user_sep + 1:user_sep + (res_sep - user_sep)]
+                resource = jidstring[res_sep + 1:] or None
+            else:
+                # server/resource (with an @ in resource)
+                server = jidstring[0:res_sep]
+                resource = jidstring[res_sep + 1:] or None
+
+    # Return the tuple
+    return prep(user, server, resource)
+
+
+def prep(user, server, resource):
+    """ Stringprep, backported from Twisted 2.0 """
+
+    if user:
+        try:
+            user = nodeprep.prepare(unicode(user))
+        except UnicodeError:
+            raise InvalidFormat, "Invalid character in username"
+    else:
+        user = None
+
+    if not server:
+        raise InvalidFormat, "Server address required."
+    else:
+        try:
+            server = nameprep.prepare(unicode(server))
+        except UnicodeError:
+            raise InvalidFormat, "Invalid character in resource"
+
+    if resource:
+        try:
+            resource = resourceprep.prepare(unicode(resource))
+        except UnicodeError:
+            raise InvalidFormat, "Invalid character in resource"
+    else:
+        resource = None
+
+    return (user, server, resource)
+
+
+
+__internJIDs = {}
+
+def intern(str):
+    # XXX: Ensure that stringprep'd jids map to same JID
+    if str in __internJIDs:
+        return __internJIDs[str]
+    else:
+        j = JID(str)
+        __internJIDs[str] = j
+        return j
+
+class JID:
+    def __init__(self, str = None, tuple = None):
+        assert (str or tuple)
+        
+        if str:
+            user, host, res = parse(str)
+        else:
+            user, host, res = tuple
+
+        self.host = host
+        self.user = user
+        self.resource = res
+            
+    def userhost(self):
+        if self.user:
+            return "%s@%s" % (self.user, self.host)
+        else:
+            return self.host
+
+    def userhostJID(self):
+        if self.resource:
+            if "_uhjid" not in self.__dict__:
+                self._uhjid = jid.intern(self.userhost())
+            return self._uhjid
+        else:
+            return self
+
+    def full(self):
+        if self.user:
+            if self.resource:
+                return "%s@%s/%s" % (self.user, self.host, self.resource)
+            else:
+                return "%s@%s" % (self.user, self.host)
+        else:
+            if self.resource:
+                return "%s/%s" % (self.host, self.resource)
+            else:
+                return self.host
+
+    def __eq__(self, other):
+        return (self.user == other.user and
+                self.host == other.host and
+                self.resource == other.resource)
+
+    def __ne__(self, other):
+        return not (self.user == other.user and
+                    self.host == other.host and
+                    self.resource == other.resource)
diff --git a/src/tlib/jabber/jstrports.py b/src/tlib/jabber/jstrports.py
new file mode 100644 (file)
index 0000000..9fbcfc5
--- /dev/null
@@ -0,0 +1,42 @@
+# Twisted, the Framework of Your Internet
+# Copyright (C) 2001-2003 Matthew W. Lefkowitz
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of version 2.1 of the GNU Lesser General Public
+# License as published by the Free Software Foundation.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+""" A temporary placeholder for client-capable strports, until we
+sufficient use cases get identified """
+
+from twisted.application import strports
+
+def _parseTCPSSL(factory, domain, port):
+    """ For the moment, parse TCP or SSL connections the same """
+    return (domain, int(port), factory), {}
+
+def _parseUNIX(factory, address):
+    return (address, factory), {}
+
+
+_funcs = { "tcp"  : _parseTCPSSL,
+           "unix" : _parseUNIX,
+           "ssl"  : _parseTCPSSL }
+
+
+def parse(description, factory):
+    args, kw = strports._parse(description)
+    return (args[0].upper(),) + _funcs[args[0]](factory, *args[1:], **kw)
+
+def client(description, factory):
+    from twisted.application import internet
+    name, args, kw = parse(description, factory)
+    return getattr(internet, name + 'Client')(*args, **kw)
diff --git a/src/tlib/jabber/xmpp_stringprep.py b/src/tlib/jabber/xmpp_stringprep.py
new file mode 100644 (file)
index 0000000..626263e
--- /dev/null
@@ -0,0 +1,231 @@
+# -*- test-case-name: twisted.words.test.test_jabberxmppstringprep -*-
+#
+# Copyright (c) 2001-2005 Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import sys, warnings
+
+if sys.version_info < (2,3,2):
+    import re
+
+    class IDNA:
+        dots = re.compile(u"[\u002E\u3002\uFF0E\uFF61]")
+        def nameprep(self, label):
+            return label.lower()
+
+    idna = IDNA()
+
+    crippled = True
+
+    warnings.warn("Accented and non-Western Jabber IDs will not be properly "
+                  "case-folded with this version of Python, resulting in "
+                  "incorrect protocol-level behavior.  It is strongly "
+                  "recommended you upgrade to Python 2.3.2 or newer if you "
+                  "intend to use Twisted's Jabber support.")
+
+else:
+    import stringprep
+    import unicodedata
+    from encodings import idna
+
+    crippled = False
+
+del sys, warnings
+
+class ILookupTable:
+    """ Interface for character lookup classes. """
+
+    def lookup(self, c):
+        """ Return whether character is in this table. """
+
+class IMappingTable:
+    """ Interface for character mapping classes. """
+
+    def map(self, c):
+        """ Return mapping for character. """
+
+class LookupTableFromFunction:
+
+    __implements__ = ILookupTable
+
+    def __init__(self, in_table_function):
+        self.lookup = in_table_function
+
+class LookupTable:
+
+    __implements__ = ILookupTable
+
+    def __init__(self, table):
+        self._table = table
+
+    def lookup(self, c):
+        return c in self._table
+
+class MappingTableFromFunction:
+
+    __implements__ = IMappingTable
+
+    def __init__(self, map_table_function):
+        self.map = map_table_function
+
+class EmptyMappingTable:
+    
+    __implements__ = IMappingTable
+
+    def __init__(self, in_table_function):
+        self._in_table_function = in_table_function
+
+    def map(self, c):
+        if self._in_table_function(c):
+            return None
+        else:
+            return c
+
+class Profile:
+    def __init__(self, mappings=[],  normalize=True, prohibiteds=[],
+                       check_unassigneds=True, check_bidi=True):
+        self.mappings = mappings
+        self.normalize = normalize
+        self.prohibiteds = prohibiteds
+        self.do_check_unassigneds = check_unassigneds
+        self.do_check_bidi = check_bidi
+
+    def prepare(self, string):
+        result = self.map(string)
+        if self.normalize:
+            result = unicodedata.normalize("NFKC", result)
+        self.check_prohibiteds(result)
+        if self.do_check_unassigneds:
+            self.check_unassigneds(result)
+        if self.do_check_bidi:
+            self.check_bidirectionals(result)
+        return result
+
+    def map(self, string):
+        result = []
+
+        for c in string:
+            result_c = c
+
+            for mapping in self.mappings:
+                result_c = mapping.map(c)
+                if result_c != c:
+                    break
+
+            if result_c is not None:
+                result.append(result_c)
+
+        return u"".join(result)
+
+    def check_prohibiteds(self, string):
+        for c in string:
+            for table in self.prohibiteds:
+                if table.lookup(c):
+                    raise UnicodeError, "Invalid character %s" % repr(c)
+
+    def check_unassigneds(self, string):
+        for c in string:
+            if stringprep.in_table_a1(c):
+                raise UnicodeError, "Unassigned code point %s" % repr(c)
+    
+    def check_bidirectionals(self, string):
+        found_LCat = False
+        found_RandALCat = False
+
+        for c in string:
+            if stringprep.in_table_d1(c):
+                found_RandALCat = True
+            if stringprep.in_table_d2(c):
+                found_LCat = True
+
+        if found_LCat and found_RandALCat:
+            raise UnicodeError, "Violation of BIDI Requirement 2"
+
+        if found_RandALCat and not (stringprep.in_table_d1(string[0]) and
+                                    stringprep.in_table_d1(string[-1])):
+            raise UnicodeError, "Violation of BIDI Requirement 3"
+
+
+class NamePrep:
+    """ Implements nameprep on international domain names.
+    
+    STD3ASCIIRules is assumed true in this implementation.
+    """
+
+    # Prohibited characters.
+    prohibiteds = [unichr(n) for n in range(0x00, 0x2c + 1) +
+                                       range(0x2e, 0x2f + 1) +
+                                       range(0x3a, 0x40 + 1) +
+                                       range(0x5b, 0x60 + 1) +
+                                       range(0x7b, 0x7f + 1) ]
+
+    def prepare(self, string):
+        result = []
+
+        labels = idna.dots.split(string)
+
+        if labels and len(labels[-1]) == 0:
+            trailing_dot = '.'
+            del labels[-1]
+        else:
+            trailing_dot = ''
+
+        for label in labels:
+            result.append(self.nameprep(label))
+
+        return ".".join(result)+trailing_dot
+
+    def check_prohibiteds(self, string):
+        for c in string:
+           if c in self.prohibiteds:
+               raise UnicodeError, "Invalid character %s" % repr(c)
+
+    def nameprep(self, label):
+        label = idna.nameprep(label)
+        self.check_prohibiteds(label)
+        if label[0] == '-':
+            raise UnicodeError, "Invalid leading hyphen-minus"
+        if label[-1] == '-':
+            raise UnicodeError, "Invalid trailing hyphen-minus"
+        return label
+
+if crippled:
+    case_map = MappingTableFromFunction(lambda c: c.lower())
+    nodeprep = Profile(mappings=[case_map],
+                       normalize=False,
+                       prohibiteds=[LookupTable([u' ', u'"', u'&', u"'", u'/',
+                                                 u':', u'<', u'>', u'@'])],
+                       check_unassigneds=False,
+                       check_bidi=False) 
+
+    resourceprep = Profile(normalize=False,
+                           check_unassigneds=False,
+                           check_bidi=False)
+   
+else:
+    C_11 = LookupTableFromFunction(stringprep.in_table_c11)
+    C_12 = LookupTableFromFunction(stringprep.in_table_c12)
+    C_21 = LookupTableFromFunction(stringprep.in_table_c21)
+    C_22 = LookupTableFromFunction(stringprep.in_table_c22)
+    C_3 = LookupTableFromFunction(stringprep.in_table_c3)
+    C_4 = LookupTableFromFunction(stringprep.in_table_c4)
+    C_5 = LookupTableFromFunction(stringprep.in_table_c5)
+    C_6 = LookupTableFromFunction(stringprep.in_table_c6)
+    C_7 = LookupTableFromFunction(stringprep.in_table_c7)
+    C_8 = LookupTableFromFunction(stringprep.in_table_c8)
+    C_9 = LookupTableFromFunction(stringprep.in_table_c9)
+
+    B_1 = EmptyMappingTable(stringprep.in_table_b1)
+    B_2 = MappingTableFromFunction(stringprep.map_table_b2)
+
+    nodeprep = Profile(mappings=[B_1, B_2],
+                       prohibiteds=[C_11, C_12, C_21, C_22,
+                                    C_3, C_4, C_5, C_6, C_7, C_8, C_9,
+                                    LookupTable([u'"', u'&', u"'", u'/',
+                                                 u':', u'<', u'>', u'@'])])
+
+    resourceprep = Profile(mappings=[B_1,],
+                           prohibiteds=[C_12, C_21, C_22,
+                                        C_3, C_4, C_5, C_6, C_7, C_8, C_9])
+
+nameprep = NamePrep()
diff --git a/src/tlib/msn.py b/src/tlib/msn.py
new file mode 100644 (file)
index 0000000..9dc1e50
--- /dev/null
@@ -0,0 +1,2435 @@
+# Twisted, the Framework of Your Internet
+# Copyright (C) 2001-2002 Matthew W. Lefkowitz
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of version 2.1 of the GNU Lesser General Public
+# License as published by the Free Software Foundation.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+"""
+MSNP8 Protocol (client only) - semi-experimental
+
+Stability: unstable.
+
+This module provides support for clients using the MSN Protocol (MSNP8).
+There are basically 3 servers involved in any MSN session:
+
+I{Dispatch server}
+
+The DispatchClient class handles connections to the
+dispatch server, which basically delegates users to a
+suitable notification server.
+
+You will want to subclass this and handle the gotNotificationReferral
+method appropriately.
+    
+I{Notification Server}
+
+The NotificationClient class handles connections to the
+notification server, which acts as a session server
+(state updates, message negotiation etc...)
+
+I{Switcboard Server}
+
+The SwitchboardClient handles connections to switchboard
+servers which are used to conduct conversations with other users.
+
+There are also two classes (FileSend and FileReceive) used
+for file transfers.
+
+Clients handle events in two ways.
+
+  - each client request requiring a response will return a Deferred,
+    the callback for same will be fired when the server sends the
+    required response
+  - Events which are not in response to any client request have
+    respective methods which should be overridden and handled in
+    an adequate manner
+
+Most client request callbacks require more than one argument,
+and since Deferreds can only pass the callback one result,
+most of the time the callback argument will be a tuple of
+values (documented in the respective request method).
+To make reading/writing code easier, callbacks can be defined in
+a number of ways to handle this 'cleanly'. One way would be to
+define methods like: def callBack(self, (arg1, arg2, arg)): ...
+another way would be to do something like:
+d.addCallback(lambda result: myCallback(*result)).
+
+If the server sends an error response to a client request,
+the errback of the corresponding Deferred will be called,
+the argument being the corresponding error code.
+
+B{NOTE}:
+Due to the lack of an official spec for MSNP8, extra checking
+than may be deemed necessary often takes place considering the
+server is never 'wrong'. Thus, if gotBadLine (in any of the 3
+main clients) is called, or an MSNProtocolError is raised, it's
+probably a good idea to submit a bug report. ;)
+Use of this module requires that PyOpenSSL is installed.
+
+TODO
+====
+- check message hooks with invalid x-msgsinvite messages.
+- font handling
+- switchboard factory
+
+@author: U{Sam Jordan<mailto:sam@twistedmatrix.com>}
+"""
+
+from __future__ import nested_scopes
+
+# Sibling imports
+from twisted.protocols.basic import LineReceiver
+#FLAG
+import utils
+if(utils.checkTwisted()):
+       from twisted.web.http import HTTPClient
+else:
+       from twisted.protocols.http import HTTPClient
+from proxy import proxy_connect_ssl
+
+# Twisted imports
+from twisted.internet import reactor, task
+from twisted.internet.defer import Deferred
+from twisted.internet.protocol import ClientFactory
+from twisted.internet.ssl import ClientContextFactory
+from twisted.python import failure, log
+
+# System imports
+import types, operator, os, md5
+from random import randint
+from urllib import quote, unquote
+
+MSN_PROTOCOL_VERSION = "MSNP8 CVR0"       # protocol version
+MSN_PORT             = 1863               # default dispatch server port
+MSN_MAX_MESSAGE      = 1664               # max message length
+MSN_CHALLENGE_STR    = "Q1P7W2E4J9R8U3S5" # used for server challenges
+MSN_CVR_STR          = "0x0409 win 4.10 i386 MSNMSGR 5.0.0544 MSMSGS" # :(
+
+# auth constants
+LOGIN_SUCCESS  = 1
+LOGIN_FAILURE  = 2
+LOGIN_REDIRECT = 3
+
+# list constants
+FORWARD_LIST = 1
+ALLOW_LIST   = 2
+BLOCK_LIST   = 4
+REVERSE_LIST = 8
+
+# phone constants
+HOME_PHONE   = "PHH"
+WORK_PHONE   = "PHW"
+MOBILE_PHONE = "PHM"
+HAS_PAGER    = "MOB"
+
+# status constants
+STATUS_ONLINE  = 'NLN'
+STATUS_OFFLINE = 'FLN'
+STATUS_HIDDEN  = 'HDN'
+STATUS_IDLE    = 'IDL'
+STATUS_AWAY    = 'AWY'
+STATUS_BUSY    = 'BSY'
+STATUS_BRB     = 'BRB'
+STATUS_PHONE   = 'PHN'
+STATUS_LUNCH   = 'LUN'
+
+CR = "\r"
+LF = "\n"
+
+LINEDEBUG = False
+
+def checkParamLen(num, expected, cmd, error=None):
+    if error == None: error = "Invalid Number of Parameters for %s" % cmd
+    if num != expected: raise MSNProtocolError, error
+
+def _parseHeader(h, v):
+    """
+    Split a certin number of known
+    header values with the format:
+    field1=val,field2=val,field3=val into
+    a dict mapping fields to values.
+    @param h: the header's key
+    @param v: the header's value as a string
+    """
+
+    if h in ('passporturls','authentication-info','www-authenticate'):
+        v = v.replace('Passport1.4','').lstrip()
+        fields = {}
+        for fieldPair in v.split(','):
+            try:
+                field,value = fieldPair.split('=',1)
+                fields[field.lower()] = value
+            except ValueError:
+                fields[field.lower()] = ''
+        return fields
+    else: return v
+
+def _parsePrimitiveHost(host):
+    # Ho Ho Ho
+    h,p = host.replace('https://','').split('/',1)
+    p = '/' + p
+    return h,p
+
+def _login(userHandle, passwd, nexusServer, cached=0, authData='', proxy=None, proxyport=None):
+    """
+    This function is used internally and should not ever be called
+    directly.
+    """
+    cb = Deferred()
+    def _cb(server, auth):
+        loginFac = ClientFactory()
+        loginFac.protocol = lambda : PassportLogin(cb, userHandle, passwd, server, auth)
+        if(proxy and proxyport):
+           proxy_connect_ssl(proxy, proxyport, _parsePrimitiveHost(server)[0], 443, loginFac)
+        else:
+            reactor.connectSSL(_parsePrimitiveHost(server)[0], 443, loginFac, ClientContextFactory())
+
+    if cached:
+        _cb(nexusServer, authData)
+    else:
+        fac = ClientFactory()
+        d = Deferred()
+        d.addCallbacks(_cb, callbackArgs=(authData,))
+        d.addErrback(lambda f: cb.errback(f))
+        fac.protocol = lambda : PassportNexus(d, nexusServer)
+       if(proxy and proxyport):
+           proxy_connect_ssl(proxy, proxyport, _parsePrimitiveHost(nexusServer)[0], 443, fac)
+       else:
+            reactor.connectSSL(_parsePrimitiveHost(nexusServer)[0], 443, fac, ClientContextFactory())
+    return cb
+
+
+class PassportNexus(HTTPClient):
+    
+    """
+    Used to obtain the URL of a valid passport
+    login HTTPS server.
+
+    This class is used internally and should
+    not be instantiated directly -- that is,
+    The passport logging in process is handled
+    transparantly by NotificationClient.
+    """
+
+    def __init__(self, deferred, host):
+        self.deferred = deferred
+        self.host, self.path = _parsePrimitiveHost(host)
+
+    def connectionMade(self):
+        HTTPClient.connectionMade(self)
+        self.sendCommand('GET', self.path)
+        self.sendHeader('Host', self.host)
+        self.endHeaders()
+        self.headers = {}
+
+    def handleHeader(self, header, value):
+        h = header.lower()
+        self.headers[h] = _parseHeader(h, value)
+
+    def handleEndHeaders(self):
+        if self.connected: self.transport.loseConnection()
+        if not self.headers.has_key('passporturls') or not self.headers['passporturls'].has_key('dalogin'):
+            self.deferred.errback(failure.Failure(failure.DefaultException("Invalid Nexus Reply")))
+        else:
+            self.deferred.callback('https://' + self.headers['passporturls']['dalogin'])
+
+    def handleResponse(self, r): pass
+
+class PassportLogin(HTTPClient):
+    """
+    This class is used internally to obtain
+    a login ticket from a passport HTTPS
+    server -- it should not be used directly.
+    """
+
+    _finished = 0
+
+    def __init__(self, deferred, userHandle, passwd, host, authData):
+        self.deferred = deferred
+        self.userHandle = userHandle
+        self.passwd = passwd
+        self.authData = authData
+        self.host, self.path = _parsePrimitiveHost(host)
+
+    def connectionMade(self):
+        self.sendCommand('GET', self.path)
+        self.sendHeader('Authorization', 'Passport1.4 OrgVerb=GET,OrgURL=http://messenger.msn.com,' +
+                                         'sign-in=%s,pwd=%s,%s' % (quote(self.userHandle), self.passwd,self.authData))
+        self.sendHeader('Host', self.host)
+        self.endHeaders()
+        self.headers = {}
+
+    def handleHeader(self, header, value):
+        h = header.lower()
+        self.headers[h] = _parseHeader(h, value)
+
+    def handleEndHeaders(self):
+        if self._finished: return
+        self._finished = 1 # I think we need this because of HTTPClient
+        if self.connected: self.transport.loseConnection()
+        authHeader = 'authentication-info'
+        _interHeader = 'www-authenticate'
+        if self.headers.has_key(_interHeader): authHeader = _interHeader
+        try:
+            info = self.headers[authHeader]
+            status = info['da-status']
+            handler = getattr(self, 'login_%s' % (status,), None)
+            if handler:
+                handler(info)
+            else: raise Exception()
+        except Exception, e:
+            self.deferred.errback(failure.Failure(e))
+
+    def handleResponse(self, r): pass
+
+    def login_success(self, info):
+        ticket = info['from-pp']
+        ticket = ticket[1:len(ticket)-1]
+        self.deferred.callback((LOGIN_SUCCESS, ticket))
+
+    def login_failed(self, info):
+        self.deferred.callback((LOGIN_FAILURE, unquote(info['cbtxt'])))
+
+    def login_redir(self, info):
+        self.deferred.callback((LOGIN_REDIRECT, self.headers['location'], self.authData))
+
+class MSNProtocolError(Exception):
+    """
+    This Exception is basically used for debugging
+    purposes, as the official MSN server should never
+    send anything _wrong_ and nobody in their right
+    mind would run their B{own} MSN server.
+    If it is raised by default command handlers
+    (handle_BLAH) the error will be logged.
+    """
+    pass
+
+class MSNMessage:
+
+    """
+    I am the class used to represent an 'instant' message.
+
+    @ivar userHandle: The user handle (passport) of the sender
+                      (this is only used when receiving a message)
+    @ivar screenName: The screen name of the sender (this is only used
+                      when receiving a message)
+    @ivar message: The message
+    @ivar headers: The message headers
+    @type headers: dict
+    @ivar length: The message length (including headers and line endings)
+    @ivar ack: This variable is used to tell the server how to respond
+               once the message has been sent. If set to MESSAGE_ACK
+               (default) the server will respond with an ACK upon receiving
+               the message, if set to MESSAGE_NACK the server will respond
+               with a NACK upon failure to receive the message.
+               If set to MESSAGE_ACK_NONE the server will do nothing.
+               This is relevant for the return value of
+               SwitchboardClient.sendMessage (which will return
+               a Deferred if ack is set to either MESSAGE_ACK or MESSAGE_NACK  
+               and will fire when the respective ACK or NACK is received).
+               If set to MESSAGE_ACK_NONE sendMessage will return None.
+    """
+    MESSAGE_ACK      = 'A'
+    MESSAGE_NACK     = 'N'
+    MESSAGE_ACK_NONE = 'U'
+
+    ack = MESSAGE_ACK
+
+    def __init__(self, length=0, userHandle="", screenName="", message=""):
+        self.userHandle = userHandle
+        self.screenName = screenName
+        self.message = message
+        self.headers = {'MIME-Version' : '1.0', 'Content-Type' : 'text/plain'}
+        self.length = length
+        self.readPos = 0
+
+    def _calcMessageLen(self):
+        """
+        used to calculte the number to send
+        as the message length when sending a message.
+        """
+        return reduce(operator.add, [len(x[0]) + len(x[1]) + 4  for x in self.headers.items()]) + len(self.message) + 2
+
+    def setHeader(self, header, value):
+        """ set the desired header """
+        self.headers[header] = value
+
+    def getHeader(self, header):
+        """
+        get the desired header value
+        @raise KeyError: if no such header exists.
+        """
+        return self.headers[header]
+
+    def hasHeader(self, header):
+        """ check to see if the desired header exists """
+        return self.headers.has_key(header)
+
+    def getMessage(self):
+        """ return the message - not including headers """
+        return self.message
+
+    def setMessage(self, message):
+        """ set the message text """
+        self.message = message
+
+class MSNContact:
+    
+    """
+    This class represents a contact (user).
+
+    @ivar userHandle: The contact's user handle (passport).
+    @ivar screenName: The contact's screen name.
+    @ivar groups: A list of all the group IDs which this
+                  contact belongs to.
+    @ivar lists: An integer representing the sum of all lists
+                 that this contact belongs to.
+    @ivar status: The contact's status code.
+    @type status: str if contact's status is known, None otherwise.
+
+    @ivar homePhone: The contact's home phone number.
+    @type homePhone: str if known, otherwise None.
+    @ivar workPhone: The contact's work phone number.
+    @type workPhone: str if known, otherwise None.
+    @ivar mobilePhone: The contact's mobile phone number.
+    @type mobilePhone: str if known, otherwise None.
+    @ivar hasPager: Whether or not this user has a mobile pager
+                    (true=yes, false=no)
+    """
+    
+    def __init__(self, userHandle="", screenName="", lists=0, groups=[], status=None):
+        self.userHandle = userHandle
+        self.screenName = screenName
+        self.lists = lists
+        self.groups = [] # if applicable
+        self.status = status # current status
+
+        # phone details
+        self.homePhone   = None
+        self.workPhone   = None
+        self.mobilePhone = None
+        self.hasPager    = None
+
+    def setPhone(self, phoneType, value):
+        """
+        set phone numbers/values for this specific user.
+        for phoneType check the *_PHONE constants and HAS_PAGER
+        """
+
+        t = phoneType.upper()
+        if t == HOME_PHONE: self.homePhone = value
+        elif t == WORK_PHONE: self.workPhone = value
+        elif t == MOBILE_PHONE: self.mobilePhone = value
+        elif t == HAS_PAGER: self.hasPager = value
+        else: raise ValueError, "Invalid Phone Type"
+
+    def addToList(self, listType):
+        """
+        Update the lists attribute to
+        reflect being part of the
+        given list.
+        """
+        self.lists |= listType
+
+    def removeFromList(self, listType):
+        """
+        Update the lists attribute to
+        reflect being removed from the
+        given list.
+        """
+        self.lists ^= listType
+
+class MSNContactList:
+    """
+    This class represents a basic MSN contact list.
+
+    @ivar contacts: All contacts on my various lists
+    @type contacts: dict (mapping user handles to MSNContact objects)
+    @ivar version: The current contact list version (used for list syncing)
+    @ivar groups: a mapping of group ids to group names
+                  (groups can only exist on the forward list)
+    @type groups: dict
+
+    B{Note}:
+    This is used only for storage and doesn't effect the
+    server's contact list.
+    """
+
+    def __init__(self):
+        self.contacts = {}
+        self.version = 0
+        self.groups = {}
+        self.autoAdd = 0
+        self.privacy = 0
+
+    def _getContactsFromList(self, listType):
+        """
+        Obtain all contacts which belong
+        to the given list type.
+        """
+        return dict([(uH,obj) for uH,obj in self.contacts.items() if obj.lists & listType])
+
+    def addContact(self, contact):
+        """
+        Add a contact
+        """
+        self.contacts[contact.userHandle] = contact
+
+    def remContact(self, userHandle):
+        """
+        Remove a contact
+        """
+        try:
+            del self.contacts[userHandle]
+        except KeyError: pass
+
+    def getContact(self, userHandle):
+        """
+        Obtain the MSNContact object
+        associated with the given
+        userHandle.
+        @return: the MSNContact object if
+                 the user exists, or None.
+        """
+        try:
+            return self.contacts[userHandle]
+        except KeyError:
+            return None
+
+    def getBlockedContacts(self):
+        """
+        Obtain all the contacts on my block list
+        """
+        return self._getContactsFromList(BLOCK_LIST)
+
+    def getAuthorizedContacts(self):
+        """
+        Obtain all the contacts on my auth list.
+        (These are contacts which I have verified
+        can view my state changes).
+        """
+        return self._getContactsFromList(ALLOW_LIST)
+
+    def getReverseContacts(self):
+        """
+        Get all contacts on my reverse list.
+        (These are contacts which have added me
+        to their forward list).
+        """
+        return self._getContactsFromList(REVERSE_LIST)
+
+    def getContacts(self):
+        """
+        Get all contacts on my forward list.
+        (These are the contacts which I have added
+        to my list).
+        """
+        return self._getContactsFromList(FORWARD_LIST)
+
+    def setGroup(self, id, name):
+        """
+        Keep a mapping from the given id
+        to the given name.
+        """
+        self.groups[id] = name
+
+    def remGroup(self, id):
+        """
+        Removed the stored group
+        mapping for the given id.
+        """
+        try:
+            del self.groups[id]
+        except KeyError: pass
+        for c in self.contacts:
+            if id in c.groups: c.groups.remove(id)
+
+
+class MSNEventBase(LineReceiver):
+    """
+    This class provides support for handling / dispatching events and is the
+    base class of the three main client protocols (DispatchClient,
+    NotificationClient, SwitchboardClient)
+    """
+
+    def __init__(self):
+        self.ids = {} # mapping of ids to Deferreds
+        self.currentID = 0
+        self.connected = 0
+        self.setLineMode()
+        self.currentMessage = None
+
+    def connectionLost(self, reason):
+        self.ids = {}
+        self.connected = 0
+
+    def connectionMade(self):
+        self.connected = 1
+
+    def _fireCallback(self, id, *args):
+        """
+        Fire the callback for the given id
+        if one exists and return 1, else return false
+        """
+        if self.ids.has_key(id):
+            self.ids[id][0].callback(args)
+            del self.ids[id]
+            return 1
+        return 0
+
+    def _nextTransactionID(self):
+        """ return a usable transaction ID """
+        self.currentID += 1
+        if self.currentID > 1000: self.currentID = 1
+        return self.currentID
+
+    def _createIDMapping(self, data=None):
+        """
+        return a unique transaction ID that is mapped internally to a
+        deferred .. also store arbitrary data if it is needed
+        """
+        id = self._nextTransactionID()
+        d = Deferred()
+        self.ids[id] = (d, data)
+        return (id, d)
+
+    def checkMessage(self, message):
+        """
+        process received messages to check for file invitations and
+        typing notifications and other control type messages
+        """
+        raise NotImplementedError
+
+    def sendLine(self, line):
+        if(LINEDEBUG): print ">> " + line
+        LineReceiver.sendLine(self, line)
+
+    def lineReceived(self, line):
+        if(LINEDEBUG): print "<< " + line
+        if self.currentMessage:
+            self.currentMessage.readPos += len(line+CR+LF)
+            try:
+                header, value = line.split(':')
+                self.currentMessage.setHeader(header, unquote(value).lstrip())
+                return
+            except ValueError:
+                #raise MSNProtocolError, "Invalid Message Header"
+                line = ""
+            if line == "" or self.currentMessage.userHandle == "NOTIFICATION":
+                self.setRawMode()
+                if self.currentMessage.readPos == self.currentMessage.length: self.rawDataReceived("") # :(
+                return
+        try:
+            cmd, params = line.split(' ', 1)
+        except ValueError:
+            #raise MSNProtocolError, "Invalid Message, %s" % repr(line)
+            cmd = line.strip() # The QNG command has no parameters.
+            params = ""
+
+        if len(cmd) != 3: raise MSNProtocolError, "Invalid Command, %s" % repr(cmd)
+        if cmd.isdigit():
+            if self.ids.has_key(params.split(' ')[0]):
+                self.ids[id].errback(int(cmd))
+                del self.ids[id]
+                return
+            else:       # we received an error which doesn't map to a sent command
+                self.gotError(int(cmd))
+                return
+
+        handler = getattr(self, "handle_%s" % cmd.upper(), None)
+        if handler:
+            try: handler(params.split(' '))
+            except MSNProtocolError, why: self.gotBadLine(line, why)
+        else:
+            self.handle_UNKNOWN(cmd, params.split(' '))
+
+    def rawDataReceived(self, data):
+        extra = ""
+        self.currentMessage.readPos += len(data)
+        diff = self.currentMessage.readPos - self.currentMessage.length
+        if diff > 0:
+            self.currentMessage.message += data[:-diff]
+            extra = data[-diff:]
+        elif diff == 0:
+            self.currentMessage.message += data
+        else:
+            self.currentMessage.message += data
+            return
+        del self.currentMessage.readPos
+        m = self.currentMessage
+        self.currentMessage = None
+        if not self.checkMessage(m):
+            self.setLineMode(extra)
+            return
+        self.setLineMode(extra)
+        self.gotMessage(m)
+
+    ### protocol command handlers - no need to override these.
+
+    def handle_MSG(self, params):
+        checkParamLen(len(params), 3, 'MSG')
+        try:
+            messageLen = int(params[2])
+        except ValueError: raise MSNProtocolError, "Invalid Parameter for MSG length argument"
+        self.currentMessage = MSNMessage(length=messageLen, userHandle=params[0], screenName=unquote(params[1]))
+
+    def handle_UNKNOWN(self, cmd, params):
+        """ implement me in subclasses if you want to handle unknown events """
+        log.msg("Received unknown command (%s), params: %s" % (cmd, params))
+
+    ### callbacks
+
+    def gotMessage(self, message):
+        """
+        called when we receive a message - override in notification
+        and switchboard clients
+        """
+        raise NotImplementedError
+
+    def gotBadLine(self, line, why):
+        """ called when a handler notifies me that this line is broken """
+        log.msg('Error in line: %s (%s)' % (line, why))
+
+    def gotError(self, errorCode):
+        """
+        called when the server sends an error which is not in
+        response to a sent command (ie. it has no matching transaction ID)
+        """
+        log.msg('Error %s' % (errorCodes[errorCode]))
+
+class DispatchClient(MSNEventBase):
+    """
+    This class provides support for clients connecting to the dispatch server
+    @ivar userHandle: your user handle (passport) needed before connecting.
+    """
+
+    # eventually this may become an attribute of the
+    # factory.
+    userHandle = ""
+
+    def connectionMade(self):
+        MSNEventBase.connectionMade(self)
+        self.sendLine('VER %s %s' % (self._nextTransactionID(), MSN_PROTOCOL_VERSION))
+
+    ### protocol command handlers ( there is no need to override these )
+
+    def handle_VER(self, params):
+        versions = params[1:]
+        if versions is None or ' '.join(versions) != MSN_PROTOCOL_VERSION:
+            self.transport.loseConnection()
+            raise MSNProtocolError, "Invalid version response"
+        id = self._nextTransactionID()
+        self.sendLine("CVR %s %s %s" % (id, MSN_CVR_STR, self.userHandle))
+
+    def handle_CVR(self, params):
+        self.sendLine("USR %s TWN I %s" % (self._nextTransactionID(), self.userHandle))
+
+    def handle_XFR(self, params):
+        if len(params) < 4: raise MSNProtocolError, "Invalid number of parameters for XFR"
+        id, refType, addr = params[:3]
+        # was addr a host:port pair?
+        try:
+            host, port = addr.split(':')
+        except ValueError:
+            host = addr
+            port = MSN_PORT
+        if refType == "NS":
+            self.gotNotificationReferral(host, int(port))
+
+    ### callbacks
+
+    def gotNotificationReferral(self, host, port):
+        """
+        called when we get a referral to the notification server.
+
+        @param host: the notification server's hostname
+        @param port: the port to connect to
+        """
+        pass
+
+
+class NotificationClient(MSNEventBase):
+    """
+    This class provides support for clients connecting
+    to the notification server.
+    """
+
+    factory = None # sssh pychecker
+
+    def __init__(self, currentID=0, proxy=None, proxyport=None):
+        MSNEventBase.__init__(self)
+        self.currentID = currentID
+        self._state = ['DISCONNECTED', {}]
+        self.proxy, self.proxyport = proxy, proxyport
+        self.pingCounter = 0
+        self.pingCheckTask = None
+
+    def _setState(self, state):
+        self._state[0] = state
+
+    def _getState(self):
+        return self._state[0]
+
+    def _getStateData(self, key):
+        return self._state[1][key]
+
+    def _setStateData(self, key, value):
+        self._state[1][key] = value
+
+    def _remStateData(self, *args):
+        for key in args: del self._state[1][key]
+
+    def connectionMade(self):
+        MSNEventBase.connectionMade(self)
+        self._setState('CONNECTED')
+        self.sendLine("VER %s %s" % (self._nextTransactionID(), MSN_PROTOCOL_VERSION))
+
+    def connectionLost(self, reason):
+        self._setState('DISCONNECTED')
+        self._state[1] = {}
+        if(self.pingCheckTask):
+            self.pingCheckTask.stop()
+            self.pingCheckTask = None
+        MSNEventBase.connectionLost(self, reason)
+
+    def checkMessage(self, message):
+        """ hook used for detecting specific notification messages """
+        cTypes = [s.lstrip() for s in message.getHeader('Content-Type').split(';')]
+        if 'text/x-msmsgsprofile' in cTypes:
+            self.gotProfile(message)
+            return 0
+        return 1
+
+    ### protocol command handlers - no need to override these
+
+    def handle_VER(self, params):
+        versions = params[1:]
+        if versions is None or ' '.join(versions) != MSN_PROTOCOL_VERSION:
+            self.transport.loseConnection()
+            raise MSNProtocolError, "Invalid version response"
+        self.sendLine("CVR %s %s %s" % (self._nextTransactionID(), MSN_CVR_STR, self.factory.userHandle))
+
+    def handle_CVR(self, params):
+        self.sendLine("USR %s TWN I %s" % (self._nextTransactionID(), self.factory.userHandle))
+
+    def handle_USR(self, params):
+        if len(params) != 4 and len(params) != 6:
+            raise MSNProtocolError, "Invalid Number of Parameters for USR"
+
+        mechanism = params[1]
+        if mechanism == "OK":
+            self.loggedIn(params[2], unquote(params[3]), int(params[4]))
+        elif params[2].upper() == "S":
+            # we need to obtain auth from a passport server
+            f = self.factory
+            d = _login(f.userHandle, f.password, f.passportServer, authData=params[3], proxy=self.proxy, proxyport=self.proxyport)
+            d.addCallback(self._passportLogin)
+            d.addErrback(self._passportError)
+
+    def _passportLogin(self, result):
+        if result[0] == LOGIN_REDIRECT:
+            d = _login(self.factory.userHandle, self.factory.password,
+                       result[1], cached=1, authData=result[2], proxy=self.proxy, proxyport=self.proxyport)
+            d.addCallback(self._passportLogin)
+            d.addErrback(self._passportError)
+        elif result[0] == LOGIN_SUCCESS:
+            self.sendLine("USR %s TWN S %s" % (self._nextTransactionID(), result[1]))
+        elif result[0] == LOGIN_FAILURE:
+            self.loginFailure(result[1])
+
+    def _passportError(self, failure):
+        self.loginFailure("Exception while authenticating: %s" % failure)
+
+    def handle_CHG(self, params):
+        checkParamLen(len(params), 3, 'CHG')
+        id = int(params[0])
+        if not self._fireCallback(id, params[1]):
+            self.statusChanged(params[1])
+
+    def handle_ILN(self, params):
+        checkParamLen(len(params), 5, 'ILN')
+        self.gotContactStatus(params[1], params[2], unquote(params[3]))
+
+    def handle_CHL(self, params):
+        checkParamLen(len(params), 2, 'CHL')
+        self.sendLine("QRY %s msmsgs@msnmsgr.com 32" % self._nextTransactionID())
+        self.transport.write(md5.md5(params[1] + MSN_CHALLENGE_STR).hexdigest())
+
+    def handle_QRY(self, params):
+        pass
+
+    def handle_NLN(self, params):
+        checkParamLen(len(params), 4, 'NLN')
+        self.contactStatusChanged(params[0], params[1], unquote(params[2]))
+
+    def handle_FLN(self, params):
+        checkParamLen(len(params), 1, 'FLN')
+        self.contactOffline(params[0])
+
+    def handle_LST(self, params):
+        # support no longer exists for manually
+        # requesting lists - why do I feel cleaner now?
+        if self._getState() != 'SYNC': return
+        contact = MSNContact(userHandle=params[0], screenName=unquote(params[1]),
+                             lists=int(params[2]))
+        if contact.lists & FORWARD_LIST:
+            contact.groups.extend(map(int, params[3].split(',')))
+        self._getStateData('list').addContact(contact)
+        self._setStateData('last_contact', contact)
+        sofar = self._getStateData('lst_sofar') + 1
+        if sofar == self._getStateData('lst_reply'):
+            # this is the best place to determine that
+            # a syn realy has finished - msn _may_ send
+            # BPR information for the last contact
+            # which is unfortunate because it means
+            # that the real end of a syn is non-deterministic.
+            # to handle this we'll keep 'last_contact' hanging
+            # around in the state data and update it if we need
+            # to later.
+            self._setState('SESSION')
+            contacts = self._getStateData('list')
+            phone = self._getStateData('phone')
+            id = self._getStateData('synid')
+            self._remStateData('lst_reply', 'lsg_reply', 'lst_sofar', 'phone', 'synid', 'list')
+            self._fireCallback(id, contacts, phone)
+        else:
+            self._setStateData('lst_sofar',sofar)
+
+    def handle_BLP(self, params):
+        # check to see if this is in response to a SYN
+        if self._getState() == 'SYNC':
+            self._getStateData('list').privacy = listCodeToID[params[0].lower()]
+        else:
+            id = int(params[0])
+            self._fireCallback(id, int(params[1]), listCodeToID[params[2].lower()])
+
+    def handle_GTC(self, params):
+        # check to see if this is in response to a SYN
+        if self._getState() == 'SYNC':
+            if params[0].lower() == "a": self._getStateData('list').autoAdd = 0
+            elif params[0].lower() == "n": self._getStateData('list').autoAdd = 1
+            else: raise MSNProtocolError, "Invalid Paramater for GTC" # debug
+        else:
+            id = int(params[0])
+            if params[1].lower() == "a": self._fireCallback(id, 0)
+            elif params[1].lower() == "n": self._fireCallback(id, 1)
+            else: raise MSNProtocolError, "Invalid Paramater for GTC" # debug
+
+    def handle_SYN(self, params):
+        id = int(params[0])
+        if len(params) == 2:
+            self._setState('SESSION')
+            self._fireCallback(id, None, None)
+        else:
+            contacts = MSNContactList()
+            contacts.version = int(params[1])
+            self._setStateData('list', contacts)
+            self._setStateData('lst_reply', int(params[2]))
+            self._setStateData('lsg_reply', int(params[3]))
+            self._setStateData('lst_sofar', 0)
+            self._setStateData('phone', [])
+
+    def handle_LSG(self, params):
+        if self._getState() == 'SYNC':
+            self._getStateData('list').groups[int(params[0])] = unquote(params[1])
+
+        # Please see the comment above the requestListGroups / requestList methods
+        # regarding support for this
+        #
+        #else:
+        #    self._getStateData('groups').append((int(params[4]), unquote(params[5])))
+        #    if params[3] == params[4]: # this was the last group
+        #        self._fireCallback(int(params[0]), self._getStateData('groups'), int(params[1]))
+        #        self._remStateData('groups')
+
+    def handle_PRP(self, params):
+        if self._getState() == 'SYNC':
+            self._getStateData('phone').append((params[0], unquote(params[1])))
+        else:
+            self._fireCallback(int(params[0]), int(params[1]), unquote(params[3]))
+
+    def handle_BPR(self, params):
+        numParams = len(params)
+        if numParams == 2: # part of a syn
+            self._getStateData('last_contact').setPhone(params[0], unquote(params[1]))
+        elif numParams == 4:
+            self.gotPhoneNumber(int(params[0]), params[1], params[2], unquote(params[3]))
+
+    def handle_ADG(self, params):
+        checkParamLen(len(params), 5, 'ADG')
+        id = int(params[0])
+        if not self._fireCallback(id, int(params[1]), unquote(params[2]), int(params[3])):
+            raise MSNProtocolError, "ADG response does not match up to a request" # debug
+
+    def handle_RMG(self, params):
+        checkParamLen(len(params), 3, 'RMG')
+        id = int(params[0])
+        if not self._fireCallback(id, int(params[1]), int(params[2])):
+            raise MSNProtocolError, "RMG response does not match up to a request" # debug
+
+    def handle_REG(self, params):
+        checkParamLen(len(params), 5, 'REG')
+        id = int(params[0])
+        if not self._fireCallback(id, int(params[1]), int(params[2]), unquote(params[3])):
+            raise MSNProtocolError, "REG response does not match up to a request" # debug
+
+    def handle_ADD(self, params):
+        numParams = len(params)
+        if numParams < 5 or params[1].upper() not in ('AL','BL','RL','FL'):
+            raise MSNProtocolError, "Invalid Paramaters for ADD" # debug
+        id = int(params[0])
+        listType = params[1].lower()
+        listVer = int(params[2])
+        userHandle = params[3]
+        groupID = None
+        if numParams == 6: # they sent a group id
+            if params[1].upper() != "FL": raise MSNProtocolError, "Only forward list can contain groups" # debug
+            groupID = int(params[5])
+        if not self._fireCallback(id, listCodeToID[listType], userHandle, listVer, groupID):
+            self.userAddedMe(userHandle, unquote(params[4]), listVer)
+
+    def handle_REM(self, params):
+        numParams = len(params)
+        if numParams < 4 or params[1].upper() not in ('AL','BL','FL','RL'):
+            raise MSNProtocolError, "Invalid Paramaters for REM" # debug
+        id = int(params[0])
+        listType = params[1].lower()
+        listVer = int(params[2])
+        userHandle = params[3]
+        groupID = None
+        if numParams == 5:
+            if params[1] != "FL": raise MSNProtocolError, "Only forward list can contain groups" # debug
+            groupID = int(params[4])
+        if not self._fireCallback(id, listCodeToID[listType], userHandle, listVer, groupID):
+            if listType.upper() == "RL": self.userRemovedMe(userHandle, listVer)
+
+    def handle_REA(self, params):
+        checkParamLen(len(params), 4, 'REA')
+        id = int(params[0])
+        self._fireCallback(id, int(params[1]), unquote(params[3]))
+
+    def handle_XFR(self, params):
+        checkParamLen(len(params), 5, 'XFR')
+        id = int(params[0])
+        # check to see if they sent a host/port pair
+        try:
+            host, port = params[2].split(':')
+        except ValueError:
+            host = params[2]
+            port = MSN_PORT
+
+        if not self._fireCallback(id, host, int(port), params[4]):
+            raise MSNProtocolError, "Got XFR (referral) that I didn't ask for .. should this happen?" # debug
+
+    def handle_RNG(self, params):
+        checkParamLen(len(params), 6, 'RNG')
+        # check for host:port pair
+        try:
+            host, port = params[1].split(":")
+            port = int(port)
+        except ValueError:
+            host = params[1]
+            port = MSN_PORT
+        self.gotSwitchboardInvitation(int(params[0]), host, port, params[3], params[4],
+                                      unquote(params[5]))
+
+    def handle_NOT(self, params):
+        checkParamLen(len(params), 1, 'NOT')
+        try:
+            messageLen = int(params[0])
+        except ValueError: raise MSNProtocolError, "Invalid Parameter for NOT length argument"
+        self.currentMessage = MSNMessage(length=messageLen, userHandle="NOTIFICATION", screenName="NOTIFICATION")
+        self.setRawMode()
+
+
+    def handle_OUT(self, params):
+        checkParamLen(len(params), 1, 'OUT')
+        if params[0] == "OTH": self.multipleLogin()
+        elif params[0] == "SSD": self.serverGoingDown()
+        else: raise MSNProtocolError, "Invalid Parameters received for OUT" # debug
+
+    def handle_QNG(self, params):
+        self.pingCounter = 0 # They replied to a ping. We'll forgive them for any they may have missed, because they're alive again now
+
+    # callbacks
+
+    def pingChecker(self):
+        if(self.pingCounter > 5):
+            # The server has ignored 5 pings, lets kill the connection
+            self.transport.loseConnection()
+        else:
+            self.sendLine("PNG")
+            self.pingCounter += 1
+
+    def pingCheckerStart(self, *args):
+        self.pingCheckTask = task.LoopingCall(self.pingChecker)
+        self.pingCheckTask.start(50.0)
+
+    def loggedIn(self, userHandle, screenName, verified):
+        """
+        Called when the client has logged in.
+        The default behaviour of this method is to
+        update the factory with our screenName and
+        to sync the contact list (factory.contacts).
+        When this is complete self.listSynchronized
+        will be called.
+
+        @param userHandle: our userHandle
+        @param screenName: our screenName
+        @param verified: 1 if our passport has been (verified), 0 if not.
+                         (i'm not sure of the significace of this)
+        @type verified: int
+        """
+        self.factory.screenName = screenName
+        listVersion = self.factory.initialListVersion
+        if self.factory.contacts: listVersion = self.factory.contacts.version
+        d = self.syncList(listVersion)
+        d.addCallback(self.listSynchronized)
+        d.addCallback(self.pingCheckerStart)
+
+    def loginFailure(self, message):
+        """
+        Called when the client fails to login.
+
+        @param message: a message indicating the problem that was encountered
+        """
+        pass
+
+    def gotProfile(self, message):
+        """
+        Called after logging in when the server sends an initial
+        message with MSN/passport specific profile information
+        such as country, number of kids, etc.
+        Check the message headers for the specific values.
+
+        @param message: The profile message
+        """
+        pass
+
+    def listSynchronized(self, *args):
+        """
+        Lists are now synchronized by default upon logging in, this
+        method is called after the synchronization has finished
+        and the factory now has the up-to-date contacts.
+        """
+        pass
+
+    def statusChanged(self, statusCode):
+        """
+        Called when our status changes and it isn't in response to
+        a client command. By default we will update the status
+        attribute of the factory.
+
+        @param statusCode: 3-letter status code
+        """
+        self.factory.status = statusCode
+
+    def gotContactStatus(self, statusCode, userHandle, screenName):
+        """
+        Called after loggin in when the server sends status of online contacts.
+        By default we will update the status attribute and screenName of the 
+        contact stored on the factory.
+
+        @param statusCode: 3-letter status code
+        @param userHandle: the contact's user handle (passport)
+        @param screenName: the contact's screen name
+        """
+        msnContact = self.factory.contacts.getContact(userHandle)
+        if(not msnContact):
+            msnContact = MSNContact()
+            msnContact.addToList(FORWARD_LIST)
+            self.factory.contacts.addContact(msnContact)
+        msnContact.status = statusCode
+        msnContact.screenName = screenName
+
+    def contactStatusChanged(self, statusCode, userHandle, screenName):
+        """
+        Called when we're notified that a contact's status has changed.
+        By default we will update the status attribute and screenName 
+        of the contact stored on the factory.
+
+        @param statusCode: 3-letter status code
+        @param userHandle: the contact's user handle (passport)
+        @param screenName: the contact's screen name
+        """
+        msnContact = self.factory.contacts.getContact(userHandle)
+        if(not msnContact):
+            msnContact = MSNContact()
+            self.factory.contacts.addContact(msnContact)
+        msnContact.status = statusCode
+        msnContact.screenName = screenName
+
+    def contactOffline(self, userHandle):
+        """
+        Called when a contact goes offline. By default this method
+        will update the status attribute of the contact stored
+        on the factory.
+
+        @param userHandle: the contact's user handle
+        """
+        msnContact = self.factory.contacts.getContact(userHandle)
+        if(msnContact):
+            msnContact.status = STATUS_OFFLINE
+
+    def gotPhoneNumber(self, listVersion, userHandle, phoneType, number):
+        """
+        Called when the server sends us phone details about
+        a specific user (for example after a user is added
+        the server will send their status, phone details etc.
+        By default we will update the list version for the
+        factory's contact list and update the phone details
+        for the specific user.
+
+        @param listVersion: the new list version
+        @param userHandle: the contact's user handle (passport)
+        @param phoneType: the specific phoneType
+                          (*_PHONE constants or HAS_PAGER)
+        @param number: the value/phone number.
+        """
+        if not self.factory.contacts: return
+       self.factory.contacts.version = listVersion
+        self.factory.contacts.getContact(userHandle).setPhone(phoneType, number)
+
+    def userAddedMe(self, userHandle, screenName, listVersion):
+        """
+        Called when a user adds me to their list. (ie. they have been added to
+        the reverse list. By default this method will update the version of
+        the factory's contact list -- that is, if the contact already exists
+        it will update the associated lists attribute, otherwise it will create
+        a new MSNContact object and store it.
+
+        @param userHandle: the userHandle of the user
+        @param screenName: the screen name of the user
+        @param listVersion: the new list version
+        @type listVersion: int
+        """
+        if not self.factory.contacts: return
+        self.factory.contacts.version = listVersion
+        c = self.factory.contacts.getContact(userHandle)
+        if not c:
+            c = MSNContact(userHandle=userHandle, screenName=screenName)
+            self.factory.contacts.addContact(c)
+        c.addToList(REVERSE_LIST)
+
+    def userRemovedMe(self, userHandle, listVersion):
+        """
+        Called when a user removes us from their contact list
+        (they are no longer on our reverseContacts list.
+        By default this method will update the version of
+        the factory's contact list -- that is, the user will
+        be removed from the reverse list and if they are no longer
+        part of any lists they will be removed from the contact
+        list entirely.
+
+        @param userHandle: the contact's user handle (passport)
+        @param listVersion: the new list version
+        """
+        if not self.factory.contacts: return
+       self.factory.contacts.version = listVersion
+        c = self.factory.contacts.getContact(userHandle)
+        if not c: return
+        c.removeFromList(REVERSE_LIST)
+        if c.lists == 0: self.factory.contacts.remContact(c.userHandle)
+
+    def gotSwitchboardInvitation(self, sessionID, host, port,
+                                 key, userHandle, screenName):
+        """
+        Called when we get an invitation to a switchboard server.
+        This happens when a user requests a chat session with us.
+
+        @param sessionID: session ID number, must be remembered for logging in
+        @param host: the hostname of the switchboard server
+        @param port: the port to connect to
+        @param key: used for authorization when connecting
+        @param userHandle: the user handle of the person who invited us
+        @param screenName: the screen name of the person who invited us
+        """
+        pass
+
+    def multipleLogin(self):
+        """
+        Called when the server says there has been another login
+        under our account, the server should disconnect us right away.
+        """
+        pass
+
+    def serverGoingDown(self):
+        """
+        Called when the server has notified us that it is going down for
+        maintenance.
+        """
+        pass
+
+    # api calls
+
+    def changeStatus(self, status):
+        """
+        Change my current status. This method will add
+        a default callback to the returned Deferred
+        which will update the status attribute of the
+        factory.
+
+        @param status: 3-letter status code (as defined by
+                       the STATUS_* constants)
+        @return: A Deferred, the callback of which will be
+                 fired when the server confirms the change
+                 of status.  The callback argument will be
+                 a tuple with the new status code as the
+                 only element.
+        """
+        
+        id, d = self._createIDMapping()
+        self.sendLine("CHG %s %s" % (id, status))
+        def _cb(r):
+            if self.factory: self.factory.status = r[0]
+            return r
+        return d.addCallback(_cb)
+
+    # I am no longer supporting the process of manually requesting
+    # lists or list groups -- as far as I can see this has no use
+    # if lists are synchronized and updated correctly, which they
+    # should be. If someone has a specific justified need for this
+    # then please contact me and i'll re-enable/fix support for it.
+
+    #def requestList(self, listType):
+    #    """
+    #    request the desired list type
+    #
+    #    @param listType: (as defined by the *_LIST constants)
+    #    @return: A Deferred, the callback of which will be
+    #             fired when the list has been retrieved.
+    #             The callback argument will be a tuple with
+    #             the only element being a list of MSNContact
+    #             objects.
+    #    """
+    #    # this doesn't need to ever be used if syncing of the lists takes place
+    #    # i.e. please don't use it!
+    #    warnings.warn("Please do not use this method - use the list syncing process instead")
+    #    id, d = self._createIDMapping()
+    #    self.sendLine("LST %s %s" % (id, listIDToCode[listType].upper()))
+    #    self._setStateData('list',[])
+    #    return d
+
+    def setPrivacyMode(self, privLevel):
+        """
+        Set my privacy mode on the server.
+
+        B{Note}:
+        This only keeps the current privacy setting on
+        the server for later retrieval, it does not
+        effect the way the server works at all.
+
+        @param privLevel: This parameter can be true, in which
+                          case the server will keep the state as
+                          'al' which the official client interprets
+                          as -> allow messages from only users on
+                          the allow list.  Alternatively it can be
+                          false, in which case the server will keep
+                          the state as 'bl' which the official client
+                          interprets as -> allow messages from all
+                          users except those on the block list.
+                          
+        @return: A Deferred, the callback of which will be fired when
+                 the server replies with the new privacy setting.
+                 The callback argument will be a tuple, the 2 elements
+                 of which being the list version and either 'al'
+                 or 'bl' (the new privacy setting).
+        """
+
+        id, d = self._createIDMapping()
+        if privLevel: self.sendLine("BLP %s AL" % id)
+        else: self.sendLine("BLP %s BL" % id)
+        return d
+
+    def syncList(self, version):
+        """
+        Used for keeping an up-to-date contact list.
+        A callback is added to the returned Deferred
+        that updates the contact list on the factory
+        and also sets my state to STATUS_ONLINE.
+
+        B{Note}:
+        This is called automatically upon signing
+        in using the version attribute of
+        factory.contacts, so you may want to persist
+        this object accordingly. Because of this there
+        is no real need to ever call this method
+        directly.
+
+        @param version: The current known list version
+
+        @return: A Deferred, the callback of which will be
+                 fired when the server sends an adequate reply.
+                 The callback argument will be a tuple with two
+                 elements, the new list (MSNContactList) and
+                 your current state (a dictionary).  If the version
+                 you sent _was_ the latest list version, both elements
+                 will be None. To just request the list send a version of 0.
+        """
+
+        self._setState('SYNC')
+        id, d = self._createIDMapping(data=str(version))
+        self._setStateData('synid',id)
+        self.sendLine("SYN %s %s" % (id, version))
+        def _cb(r):
+            self.changeStatus(STATUS_ONLINE)
+            if r[0] is not None:
+                self.factory.contacts = r[0]
+            return r
+        return d.addCallback(_cb)
+
+
+    # I am no longer supporting the process of manually requesting
+    # lists or list groups -- as far as I can see this has no use
+    # if lists are synchronized and updated correctly, which they
+    # should be. If someone has a specific justified need for this
+    # then please contact me and i'll re-enable/fix support for it.
+                    
+    #def requestListGroups(self):
+    #    """
+    #    Request (forward) list groups.
+    #
+    #    @return: A Deferred, the callback for which will be called
+    #             when the server responds with the list groups.
+    #             The callback argument will be a tuple with two elements,
+    #             a dictionary mapping group IDs to group names and the
+    #             current list version.
+    #    """
+    #    
+    #    # this doesn't need to be used if syncing of the lists takes place (which it SHOULD!)
+    #    # i.e. please don't use it!
+    #    warnings.warn("Please do not use this method - use the list syncing process instead")
+    #    id, d = self._createIDMapping()
+    #    self.sendLine("LSG %s" % id)
+    #    self._setStateData('groups',{})
+    #    return d
+
+    def setPhoneDetails(self, phoneType, value):
+        """
+        Set/change my phone numbers stored on the server.
+
+        @param phoneType: phoneType can be one of the following
+                          constants - HOME_PHONE, WORK_PHONE,
+                          MOBILE_PHONE, HAS_PAGER.
+                          These are pretty self-explanatory, except
+                          maybe HAS_PAGER which refers to whether or
+                          not you have a pager.
+        @param value: for all of the *_PHONE constants the value is a
+                      phone number (str), for HAS_PAGER accepted values
+                      are 'Y' (for yes) and 'N' (for no).
+
+        @return: A Deferred, the callback for which will be fired when
+                 the server confirms the change has been made. The
+                 callback argument will be a tuple with 2 elements, the
+                 first being the new list version (int) and the second
+                 being the new phone number value (str).
+        """
+        # XXX: Add a default callback which updates
+        # factory.contacts.version and the relevant phone
+        # number
+        id, d = self._createIDMapping()
+        self.sendLine("PRP %s %s %s" % (id, phoneType, quote(value)))
+        return d
+
+    def addListGroup(self, name):
+        """
+        Used to create a new list group.
+        A default callback is added to the
+        returned Deferred which updates the
+        contacts attribute of the factory.
+
+        @param name: The desired name of the new group.
+
+        @return: A Deferred, the callbacck for which will be called
+                 when the server clarifies that the new group has been
+                 created.  The callback argument will be a tuple with 3
+                 elements: the new list version (int), the new group name
+                 (str) and the new group ID (int).
+        """
+
+        id, d = self._createIDMapping()
+        self.sendLine("ADG %s %s 0" % (id, quote(name)))
+        def _cb(r):
+            self.factory.contacts.version = r[0]
+            self.factory.contacts.setGroup(r[1], r[2])
+            return r
+        return d.addCallback(_cb)
+
+    def remListGroup(self, groupID):
+        """
+        Used to remove a list group.
+        A default callback is added to the
+        returned Deferred which updates the
+        contacts attribute of the factory.
+
+        @param groupID: the ID of the desired group to be removed.
+
+        @return: A Deferred, the callback for which will be called when
+                 the server clarifies the deletion of the group.
+                 The callback argument will be a tuple with 2 elements:
+                 the new list version (int) and the group ID (int) of
+                 the removed group.
+        """
+
+        id, d = self._createIDMapping()
+        self.sendLine("RMG %s %s" % (id, groupID))
+        def _cb(r):
+            self.factory.contacts.version = r[0]
+            self.factory.contacts.remGroup(r[1])
+            return r
+        return d.addCallback(_cb)
+
+    def renameListGroup(self, groupID, newName):
+        """
+        Used to rename an existing list group.
+        A default callback is added to the returned
+        Deferred which updates the contacts attribute
+        of the factory.
+
+        @param groupID: the ID of the desired group to rename.
+        @param newName: the desired new name for the group.
+
+        @return: A Deferred, the callback for which will be called
+                 when the server clarifies the renaming.
+                 The callback argument will be a tuple of 3 elements,
+                 the new list version (int), the group id (int) and
+                 the new group name (str).
+        """
+        
+        id, d = self._createIDMapping()
+        self.sendLine("REG %s %s %s 0" % (id, groupID, quote(newName)))
+        def _cb(r):
+            self.factory.contacts.version = r[0]
+            self.factory.contacts.setGroup(r[1], r[2])
+            return r
+        return d.addCallback(_cb)
+
+    def addContact(self, listType, userHandle, groupID=0):
+        """
+        Used to add a contact to the desired list.
+        A default callback is added to the returned
+        Deferred which updates the contacts attribute of
+        the factory with the new contact information.
+        If you are adding a contact to the forward list
+        and you want to associate this contact with multiple
+        groups then you will need to call this method for each
+        group you would like to add them to, changing the groupID
+        parameter. The default callback will take care of updating
+        the group information on the factory's contact list.
+
+        @param listType: (as defined by the *_LIST constants)
+        @param userHandle: the user handle (passport) of the contact
+                           that is being added
+        @param groupID: the group ID for which to associate this contact
+                        with. (default 0 - default group). Groups are only
+                        valid for FORWARD_LIST.
+
+        @return: A Deferred, the callback for which will be called when
+                 the server has clarified that the user has been added.
+                 The callback argument will be a tuple with 4 elements:
+                 the list type, the contact's user handle, the new list
+                 version, and the group id (if relevant, otherwise it
+                 will be None)
+        """
+        
+        id, d = self._createIDMapping()
+        listType = listIDToCode[listType].upper()
+        if listType == "FL":
+            self.sendLine("ADD %s FL %s %s %s" % (id, userHandle, userHandle, groupID))
+        else:
+            self.sendLine("ADD %s %s %s %s" % (id, listType, userHandle, userHandle))
+
+        def _cb(r):
+            self.factory.contacts.version = r[2]
+            c = self.factory.contacts.getContact(r[1])
+            if not c:
+                c = MSNContact(userHandle=r[1])
+            if r[3]: c.groups.append(r[3])
+            c.addToList(r[0])
+            return r
+        return d.addCallback(_cb)
+
+    def remContact(self, listType, userHandle, groupID=0):
+        """
+        Used to remove a contact from the desired list.
+        A default callback is added to the returned deferred
+        which updates the contacts attribute of the factory
+        to reflect the new contact information. If you are
+        removing from the forward list then you will need to
+        supply a groupID, if the contact is in more than one
+        group then they will only be removed from this group
+        and not the entire forward list, but if this is their
+        only group they will be removed from the whole list.
+
+        @param listType: (as defined by the *_LIST constants)
+        @param userHandle: the user handle (passport) of the
+                           contact being removed
+        @param groupID: the ID of the group to which this contact
+                        belongs (only relevant for FORWARD_LIST,
+                        default is 0)
+
+        @return: A Deferred, the callback for which will be called when
+                 the server has clarified that the user has been removed.
+                 The callback argument will be a tuple of 4 elements:
+                 the list type, the contact's user handle, the new list
+                 version, and the group id (if relevant, otherwise it will
+                 be None)
+        """
+        
+        id, d = self._createIDMapping()
+        listType = listIDToCode[listType].upper()
+        if listType == "FL":
+            self.sendLine("REM %s FL %s %s" % (id, userHandle, groupID))
+        else:
+            self.sendLine("REM %s %s %s" % (id, listType, userHandle))
+
+        def _cb(r):
+            l = self.factory.contacts
+            l.version = r[2]
+            c = l.getContact(r[1])
+            if not c: return
+            group = r[3]
+            shouldRemove = 1
+            if group: # they may not have been removed from the list
+                c.groups.remove(group)
+                if c.groups: shouldRemove = 0
+            if shouldRemove:
+                c.removeFromList(r[0])
+                if c.lists == 0: l.remContact(c.userHandle)
+            return r
+        return d.addCallback(_cb)
+
+    def changeScreenName(self, newName):
+        """
+        Used to change your current screen name.
+        A default callback is added to the returned
+        Deferred which updates the screenName attribute
+        of the factory and also updates the contact list
+        version.
+
+        @param newName: the new screen name
+
+        @return: A Deferred, the callback for which will be called
+                 when the server sends an adequate reply.
+                 The callback argument will be a tuple of 2 elements:
+                 the new list version and the new screen name.
+        """
+
+        id, d = self._createIDMapping()
+        self.sendLine("REA %s %s %s" % (id, self.factory.userHandle, quote(newName)))
+        def _cb(r):
+            if(self.factory.contacts): self.factory.contacts.version = r[0]
+            self.factory.screenName = r[1]
+            return r
+        return d.addCallback(_cb)
+
+    def requestSwitchboardServer(self):
+        """
+        Used to request a switchboard server to use for conversations.
+
+        @return: A Deferred, the callback for which will be called when
+                 the server responds with the switchboard information.
+                 The callback argument will be a tuple with 3 elements:
+                 the host of the switchboard server, the port and a key
+                 used for logging in.
+        """
+
+        id, d = self._createIDMapping()
+        self.sendLine("XFR %s SB" % id)
+        return d
+
+    def logOut(self):
+        """
+        Used to log out of the notification server.
+        After running the method the server is expected
+        to close the connection.
+        """
+        
+        if(self.pingCheckTask):
+            self.pingCheckTask.stop()
+            self.pingCheckTask = None
+        self.sendLine("OUT")
+
+class NotificationFactory(ClientFactory):
+    """
+    Factory for the NotificationClient protocol.
+    This is basically responsible for keeping
+    the state of the client and thus should be used
+    in a 1:1 situation with clients.
+
+    @ivar contacts: An MSNContactList instance reflecting
+                    the current contact list -- this is
+                    generally kept up to date by the default
+                    command handlers.
+    @ivar userHandle: The client's userHandle, this is expected
+                      to be set by the client and is used by the
+                      protocol (for logging in etc).
+    @ivar screenName: The client's current screen-name -- this is
+                      generally kept up to date by the default
+                      command handlers.
+    @ivar password: The client's password -- this is (obviously)
+                    expected to be set by the client.
+    @ivar passportServer: This must point to an msn passport server
+                          (the whole URL is required)
+    @ivar status: The status of the client -- this is generally kept
+                  up to date by the default command handlers
+    """
+
+    contacts = None
+    userHandle = ''
+    screenName = ''
+    password = ''
+    passportServer = 'https://nexus.passport.com/rdr/pprdr.asp'
+    status = 'FLN'
+    protocol = NotificationClient
+    initialListVersion = 0
+
+
+# XXX: A lot of the state currently kept in
+# instances of SwitchboardClient is likely to
+# be moved into a factory at some stage in the
+# future
+
+class SwitchboardClient(MSNEventBase):
+    """
+    This class provides support for clients connecting to a switchboard server.
+
+    Switchboard servers are used for conversations with other people
+    on the MSN network. This means that the number of conversations at
+    any given time will be directly proportional to the number of
+    connections to varioius switchboard servers.
+
+    MSN makes no distinction between single and group conversations,
+    so any number of users may be invited to join a specific conversation
+    taking place on a switchboard server.
+
+    @ivar key: authorization key, obtained when receiving
+               invitation / requesting switchboard server.
+    @ivar userHandle: your user handle (passport)
+    @ivar sessionID: unique session ID, used if you are replying
+                     to a switchboard invitation
+    @ivar reply: set this to 1 in connectionMade or before to signifiy
+                 that you are replying to a switchboard invitation.
+    """
+
+    key = 0
+    userHandle = ""
+    sessionID = ""
+    reply = 0
+
+    _iCookie = 0
+
+    def __init__(self):
+        MSNEventBase.__init__(self)
+        self.pendingUsers = {}
+        self.cookies = {'iCookies' : {}, 'external' : {}} # will maybe be moved to a factory in the future
+
+    def connectionMade(self):
+        MSNEventBase.connectionMade(self)
+        self._sendInit()
+
+    def connectionLost(self, reason):
+        self.cookies['iCookies'] = {}
+        self.cookies['external'] = {}
+        MSNEventBase.connectionLost(self, reason)
+
+    def _sendInit(self):
+        """
+        send initial data based on whether we are replying to an invitation
+        or starting one.
+        """
+        id = self._nextTransactionID()
+        if not self.reply:
+            self.sendLine("USR %s %s %s" % (id, self.userHandle, self.key))
+        else:
+            self.sendLine("ANS %s %s %s %s" % (id, self.userHandle, self.key, self.sessionID))
+
+    def _newInvitationCookie(self):
+        self._iCookie += 1
+        if self._iCookie > 1000: self._iCookie = 1
+        return self._iCookie
+
+    def _checkTyping(self, message, cTypes):
+        """ helper method for checkMessage """
+        if 'text/x-msmsgscontrol' in cTypes and message.hasHeader('TypingUser'):
+            self.userTyping(message)
+            return 1
+
+    def _checkFileInvitation(self, message, info):
+        """ helper method for checkMessage """
+        if not info.get('Application-Name', '').lower() == 'file transfer': return 0
+        try:
+            cookie = info['Invitation-Cookie']
+            fileName = info['Application-File']
+            fileSize = int(info['Application-FileSize'])
+        except KeyError:
+            log.msg('Received munged file transfer request ... ignoring.')
+            return 0
+        self.gotSendRequest(fileName, fileSize, cookie, message)
+        return 1
+
+    def _checkFileResponse(self, message, info):
+        """ helper method for checkMessage """
+        try:
+            cmd = info['Invitation-Command'].upper()
+            cookie = info['Invitation-Cookie']
+        except KeyError: return 0
+        accept = (cmd == 'ACCEPT') and 1 or 0
+        requested = self.cookies['iCookies'].get(cookie)
+        if not requested: return 1
+        requested[0].callback((accept, cookie, info))
+        del self.cookies['iCookies'][cookie]
+        return 1
+
+    def _checkFileInfo(self, message, info):
+        """ helper method for checkMessage """
+        try:
+            ip = info['IP-Address']
+            iCookie = info['Invitation-Cookie']
+            aCookie = info['AuthCookie']
+            cmd = info['Invitation-Command'].upper()
+            port = int(info['Port'])
+        except KeyError: return 0
+        accept = (cmd == 'ACCEPT') and 1 or 0
+        requested = self.cookies['external'].get(iCookie)
+        if not requested: return 1 # we didn't ask for this
+        requested[0].callback((accept, ip, port, aCookie, info))
+        del self.cookies['external'][iCookie]
+        return 1
+
+    def checkMessage(self, message):
+        """
+        hook for detecting any notification type messages
+        (e.g. file transfer)
+        """
+        cTypes = [s.lstrip() for s in message.getHeader('Content-Type').split(';')]
+        if self._checkTyping(message, cTypes): return 0
+        if 'text/x-msmsgsinvite' in cTypes:
+            # header like info is sent as part of the message body.
+            info = {}
+            for line in message.message.split('\r\n'):
+                try:
+                    key, val = line.split(':')
+                    info[key] = val.lstrip()
+                except ValueError: continue
+            if self._checkFileInvitation(message, info) or self._checkFileInfo(message, info) or self._checkFileResponse(message, info): return 0
+        return 1
+
+    # negotiation
+    def handle_USR(self, params):
+        checkParamLen(len(params), 4, 'USR')
+        if params[1] == "OK":
+            self.loggedIn()
+
+    # invite a user
+    def handle_CAL(self, params):
+        checkParamLen(len(params), 3, 'CAL')
+        id = int(params[0])
+        if params[1].upper() == "RINGING":
+            self._fireCallback(id, int(params[2])) # session ID as parameter
+
+    # user joined
+    def handle_JOI(self, params):
+        checkParamLen(len(params), 2, 'JOI')
+        self.userJoined(params[0], unquote(params[1]))
+
+    # users participating in the current chat
+    def handle_IRO(self, params):
+        checkParamLen(len(params), 5, 'IRO')
+        self.pendingUsers[params[3]] = unquote(params[4])
+        if params[1] == params[2]:
+            self.gotChattingUsers(self.pendingUsers)
+            self.pendingUsers = {}
+
+    # finished listing users
+    def handle_ANS(self, params):
+        checkParamLen(len(params), 2, 'ANS')
+        if params[1] == "OK":
+            self.loggedIn()
+
+    def handle_ACK(self, params):
+        checkParamLen(len(params), 1, 'ACK')
+        self._fireCallback(int(params[0]), None)
+
+    def handle_NAK(self, params):
+        checkParamLen(len(params), 1, 'NAK')
+        self._fireCallback(int(params[0]), None)
+
+    def handle_BYE(self, params):
+        #checkParamLen(len(params), 1, 'BYE') # i've seen more than 1 param passed to this
+        self.userLeft(params[0])
+
+    # callbacks
+
+    def loggedIn(self):
+        """
+        called when all login details have been negotiated.
+        Messages can now be sent, or new users invited.
+        """
+        pass
+
+    def gotChattingUsers(self, users):
+        """
+        called after connecting to an existing chat session.
+
+        @param users: A dict mapping user handles to screen names
+                      (current users taking part in the conversation)
+        """
+        pass
+
+    def userJoined(self, userHandle, screenName):
+        """
+        called when a user has joined the conversation.
+
+        @param userHandle: the user handle (passport) of the user
+        @param screenName: the screen name of the user
+        """
+        pass
+
+    def userLeft(self, userHandle):
+        """
+        called when a user has left the conversation.
+
+        @param userHandle: the user handle (passport) of the user.
+        """
+        pass
+
+    def gotMessage(self, message):
+        """
+        called when we receive a message.
+
+        @param message: the associated MSNMessage object
+        """
+        pass
+
+    def userTyping(self, message):
+        """
+        called when we receive the special type of message notifying
+        us that a user is typing a message.
+
+        @param message: the associated MSNMessage object
+        """
+        pass
+
+    def gotSendRequest(self, fileName, fileSize, iCookie, message):
+        """
+        called when a contact is trying to send us a file.
+        To accept or reject this transfer see the
+        fileInvitationReply method.
+
+        @param fileName: the name of the file
+        @param fileSize: the size of the file
+        @param iCookie: the invitation cookie, used so the client can
+                        match up your reply with this request.
+        @param message: the MSNMessage object which brought about this
+                        invitation (it may contain more information)
+        """
+        pass
+
+    # api calls
+
+    def inviteUser(self, userHandle):
+        """
+        used to invite a user to the current switchboard server.
+
+        @param userHandle: the user handle (passport) of the desired user.
+
+        @return: A Deferred, the callback for which will be called
+                 when the server notifies us that the user has indeed
+                 been invited.  The callback argument will be a tuple
+                 with 1 element, the sessionID given to the invited user.
+                 I'm not sure if this is useful or not.
+        """
+
+        id, d = self._createIDMapping()
+        self.sendLine("CAL %s %s" % (id, userHandle))
+        return d
+
+    def sendMessage(self, message):
+        """
+        used to send a message.
+
+        @param message: the corresponding MSNMessage object.
+
+        @return: Depending on the value of message.ack.
+                 If set to MSNMessage.MESSAGE_ACK or
+                 MSNMessage.MESSAGE_NACK a Deferred will be returned,
+                 the callback for which will be fired when an ACK or
+                 NACK is received - the callback argument will be
+                 (None,). If set to MSNMessage.MESSAGE_ACK_NONE then
+                 the return value is None.
+        """
+
+        if message.ack not in ('A','N'): id, d = self._nextTransactionID(), None
+        else: id, d = self._createIDMapping()
+        if message.length == 0: message.length = message._calcMessageLen()
+        self.sendLine("MSG %s %s %s" % (id, message.ack, message.length))
+        # apparently order matters with at least MIME-Version and Content-Type
+        self.sendLine('MIME-Version: %s' % message.getHeader('MIME-Version'))
+        self.sendLine('Content-Type: %s' % message.getHeader('Content-Type'))
+        # send the rest of the headers
+        for header in [h for h in message.headers.items() if h[0].lower() not in ('mime-version','content-type')]:
+            self.sendLine("%s: %s" % (header[0], header[1]))
+        self.transport.write(CR+LF)
+        self.transport.write(message.message)
+        return d
+
+    def sendTypingNotification(self):
+        """
+        used to send a typing notification. Upon receiving this
+        message the official client will display a 'user is typing'
+        message to all other users in the chat session for 10 seconds.
+        The official client sends one of these every 5 seconds (I think)
+        as long as you continue to type.
+        """
+        m = MSNMessage()
+        m.ack = m.MESSAGE_ACK_NONE
+        m.setHeader('Content-Type', 'text/x-msmsgscontrol')
+        m.setHeader('TypingUser', self.userHandle)
+        m.message = "\r\n"
+        self.sendMessage(m)
+
+    def sendFileInvitation(self, fileName, fileSize):
+        """
+        send an notification that we want to send a file.
+
+        @param fileName: the file name
+        @param fileSize: the file size
+
+        @return: A Deferred, the callback of which will be fired
+                 when the user responds to this invitation with an
+                 appropriate message. The callback argument will be
+                 a tuple with 3 elements, the first being 1 or 0
+                 depending on whether they accepted the transfer
+                 (1=yes, 0=no), the second being an invitation cookie
+                 to identify your follow-up responses and the third being
+                 the message 'info' which is a dict of information they
+                 sent in their reply (this doesn't really need to be used).
+                 If you wish to proceed with the transfer see the
+                 sendTransferInfo method.
+        """
+        cookie = self._newInvitationCookie()
+        d = Deferred()
+        m = MSNMessage()
+        m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
+        m.message += 'Application-Name: File Transfer\r\n'
+        m.message += 'Application-GUID: {5D3E02AB-6190-11d3-BBBB-00C04F795683}\r\n'
+        m.message += 'Invitation-Command: INVITE\r\n'
+        m.message += 'Invitation-Cookie: %s\r\n' % str(cookie)
+        m.message += 'Application-File: %s\r\n' % fileName
+        m.message += 'Application-FileSize: %s\r\n\r\n' % str(fileSize)
+        m.ack = m.MESSAGE_ACK_NONE
+        self.sendMessage(m)
+        self.cookies['iCookies'][cookie] = (d, m)
+        return d
+
+    def fileInvitationReply(self, iCookie, accept=1):
+        """
+        used to reply to a file transfer invitation.
+
+        @param iCookie: the invitation cookie of the initial invitation
+        @param accept: whether or not you accept this transfer,
+                       1 = yes, 0 = no, default = 1.
+
+        @return: A Deferred, the callback for which will be fired when
+                 the user responds with the transfer information.
+                 The callback argument will be a tuple with 5 elements,
+                 whether or not they wish to proceed with the transfer
+                 (1=yes, 0=no), their ip, the port, the authentication
+                 cookie (see FileReceive/FileSend) and the message
+                 info (dict) (in case they send extra header-like info
+                 like Internal-IP, this doesn't necessarily need to be
+                 used). If you wish to proceed with the transfer see
+                 FileReceive.
+        """
+        d = Deferred()
+        m = MSNMessage()
+        m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
+        m.message += 'Invitation-Command: %s\r\n' % (accept and 'ACCEPT' or 'CANCEL')
+        m.message += 'Invitation-Cookie: %s\r\n' % str(iCookie)
+        if not accept: m.message += 'Cancel-Code: REJECT\r\n'
+        m.message += 'Launch-Application: FALSE\r\n'
+        m.message += 'Request-Data: IP-Address:\r\n'
+        m.message += '\r\n'
+        m.ack = m.MESSAGE_ACK_NONE
+        self.sendMessage(m)
+        self.cookies['external'][iCookie] = (d, m)
+        return d
+
+    def sendTransferInfo(self, accept, iCookie, authCookie, ip, port):
+        """
+        send information relating to a file transfer session.
+
+        @param accept: whether or not to go ahead with the transfer
+                       (1=yes, 0=no)
+        @param iCookie: the invitation cookie of previous replies
+                        relating to this transfer
+        @param authCookie: the authentication cookie obtained from
+                           an FileSend instance
+        @param ip: your ip
+        @param port: the port on which an FileSend protocol is listening.
+        """
+        m = MSNMessage()
+        m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
+        m.message += 'Invitation-Command: %s\r\n' % (accept and 'ACCEPT' or 'CANCEL')
+        m.message += 'Invitation-Cookie: %s\r\n' % iCookie
+        m.message += 'IP-Address: %s\r\n' % ip
+        m.message += 'Port: %s\r\n' % port
+        m.message += 'AuthCookie: %s\r\n' % authCookie
+        m.message += '\r\n'
+        m.ack = m.MESSAGE_NACK
+        self.sendMessage(m)
+
+class FileReceive(LineReceiver):
+    """
+    This class provides support for receiving files from contacts.
+
+    @ivar fileSize: the size of the receiving file. (you will have to set this)
+    @ivar connected: true if a connection has been established.
+    @ivar completed: true if the transfer is complete.
+    @ivar bytesReceived: number of bytes (of the file) received.
+                         This does not include header data.
+    """
+
+    def __init__(self, auth, myUserHandle, file, directory="", overwrite=0):
+        """
+        @param auth: auth string received in the file invitation.
+        @param myUserHandle: your userhandle.
+        @param file: A string or file object represnting the file
+                     to save data to.
+        @param directory: optional parameter specifiying the directory.
+                          Defaults to the current directory.
+        @param overwrite: if true and a file of the same name exists on
+                          your system, it will be overwritten. (0 by default)
+        """
+        self.auth = auth
+        self.myUserHandle = myUserHandle
+        self.fileSize = 0
+        self.connected = 0
+        self.completed = 0
+        self.directory = directory
+        self.bytesReceived = 0
+        self.overwrite = overwrite
+
+        # used for handling current received state
+        self.state = 'CONNECTING'
+        self.segmentLength = 0
+        self.buffer = ''
+        
+        if isinstance(file, types.StringType):
+            path = os.path.join(directory, file)
+            if os.path.exists(path) and not self.overwrite:
+                log.msg('File already exists...')
+                raise IOError, "File Exists" # is this all we should do here?
+            self.file = open(os.path.join(directory, file), 'wb')
+        else:
+            self.file = file
+
+    def connectionMade(self):
+        self.connected = 1
+        self.state = 'INHEADER'
+        self.sendLine('VER MSNFTP')
+
+    def connectionLost(self, reason):
+        self.connected = 0
+        self.file.close()
+
+    def parseHeader(self, header):
+        """ parse the header of each 'message' to obtain the segment length """
+
+        if ord(header[0]) != 0: # they requested that we close the connection
+            self.transport.loseConnection()
+            return
+        try:
+            extra, factor = header[1:]
+        except ValueError:
+            # munged header, ending transfer
+            self.transport.loseConnection()
+            raise
+        extra  = ord(extra)
+        factor = ord(factor)
+        return factor * 256 + extra
+
+    def lineReceived(self, line):
+        temp = line.split()
+        if len(temp) == 1: params = []
+        else: params = temp[1:]
+        cmd = temp[0]
+        handler = getattr(self, "handle_%s" % cmd.upper(), None)
+        if handler: handler(params) # try/except
+        else: self.handle_UNKNOWN(cmd, params)
+
+    def rawDataReceived(self, data):
+        bufferLen = len(self.buffer)
+        if self.state == 'INHEADER':
+            delim = 3-bufferLen
+            self.buffer += data[:delim]
+            if len(self.buffer) == 3:
+                self.segmentLength = self.parseHeader(self.buffer)
+                if not self.segmentLength: return # hrm
+                self.buffer = ""
+                self.state = 'INSEGMENT'
+            extra = data[delim:]
+            if len(extra) > 0: self.rawDataReceived(extra)
+            return
+
+        elif self.state == 'INSEGMENT':
+            dataSeg = data[:(self.segmentLength-bufferLen)]
+            self.buffer += dataSeg
+            self.bytesReceived += len(dataSeg)
+            if len(self.buffer) == self.segmentLength:
+                self.gotSegment(self.buffer)
+                self.buffer = ""
+                if self.bytesReceived == self.fileSize:
+                    self.completed = 1
+                    self.buffer = ""
+                    self.file.close()
+                    self.sendLine("BYE 16777989")
+                    return
+                self.state = 'INHEADER'
+                extra = data[(self.segmentLength-bufferLen):]
+                if len(extra) > 0: self.rawDataReceived(extra)
+                return
+
+    def handle_VER(self, params):
+        checkParamLen(len(params), 1, 'VER')
+        if params[0].upper() == "MSNFTP":
+            self.sendLine("USR %s %s" % (self.myUserHandle, self.auth))
+        else:
+            log.msg('they sent the wrong version, time to quit this transfer')
+            self.transport.loseConnection()
+
+    def handle_FIL(self, params):
+        checkParamLen(len(params), 1, 'FIL')
+        try:
+            self.fileSize = int(params[0])
+        except ValueError: # they sent the wrong file size - probably want to log this
+            self.transport.loseConnection()
+            return
+        self.setRawMode()
+        self.sendLine("TFR")
+
+    def handle_UNKNOWN(self, cmd, params):
+        log.msg('received unknown command (%s), params: %s' % (cmd, params))
+
+    def gotSegment(self, data):
+        """ called when a segment (block) of data arrives. """
+        self.file.write(data)
+
+class FileSend(LineReceiver):
+    """
+    This class provides support for sending files to other contacts.
+
+    @ivar bytesSent: the number of bytes that have currently been sent.
+    @ivar completed: true if the send has completed.
+    @ivar connected: true if a connection has been established.
+    @ivar targetUser: the target user (contact).
+    @ivar segmentSize: the segment (block) size.
+    @ivar auth: the auth cookie (number) to use when sending the
+                transfer invitation
+    """
+    
+    def __init__(self, file):
+        """
+        @param file: A string or file object represnting the file to send.
+        """
+
+        if isinstance(file, types.StringType):
+            self.file = open(file, 'rb')
+        else:
+            self.file = file
+
+        self.fileSize = 0
+        self.bytesSent = 0
+        self.completed = 0
+        self.connected = 0
+        self.targetUser = None
+        self.segmentSize = 2045
+        self.auth = randint(0, 2**30)
+        self._pendingSend = None # :(
+
+    def connectionMade(self):
+        self.connected = 1
+
+    def connectionLost(self, reason):
+        if self._pendingSend:
+            self._pendingSend.cancel()
+            self._pendingSend = None
+        self.connected = 0
+        self.file.close()
+
+    def lineReceived(self, line):
+        temp = line.split()
+        if len(temp) == 1: params = []
+        else: params = temp[1:]
+        cmd = temp[0]
+        handler = getattr(self, "handle_%s" % cmd.upper(), None)
+        if handler: handler(params)
+        else: self.handle_UNKNOWN(cmd, params)
+
+    def handle_VER(self, params):
+        checkParamLen(len(params), 1, 'VER')
+        if params[0].upper() == "MSNFTP":
+            self.sendLine("VER MSNFTP")
+        else: # they sent some weird version during negotiation, i'm quitting.
+            self.transport.loseConnection()
+
+    def handle_USR(self, params):
+        checkParamLen(len(params), 2, 'USR')
+        self.targetUser = params[0]
+        if self.auth == int(params[1]):
+            self.sendLine("FIL %s" % (self.fileSize))
+        else: # they failed the auth test, disconnecting.
+            self.transport.loseConnection()
+
+    def handle_TFR(self, params):
+        checkParamLen(len(params), 0, 'TFR')
+        # they are ready for me to start sending
+        self.sendPart()
+
+    def handle_BYE(self, params):
+        self.completed = (self.bytesSent == self.fileSize)
+        self.transport.loseConnection()
+
+    def handle_CCL(self, params):
+        self.completed = (self.bytesSent == self.fileSize)
+        self.transport.loseConnection()
+
+    def handle_UNKNOWN(self, cmd, params): log.msg('received unknown command (%s), params: %s' % (cmd, params))
+
+    def makeHeader(self, size):
+        """ make the appropriate header given a specific segment size. """
+        quotient, remainder = divmod(size, 256)
+        return chr(0) + chr(remainder) + chr(quotient)
+
+    def sendPart(self):
+        """ send a segment of data """
+        if not self.connected:
+            self._pendingSend = None
+            return # may be buggy (if handle_CCL/BYE is called but self.connected is still 1)
+        data = self.file.read(self.segmentSize)
+        if data:
+            dataSize = len(data)
+            header = self.makeHeader(dataSize)
+            self.transport.write(header + data)
+            self.bytesSent += dataSize
+            self._pendingSend = reactor.callLater(0, self.sendPart)
+        else:
+            self._pendingSend = None
+            self.completed = 1
+
+# mapping of error codes to error messages
+errorCodes = {
+
+    200 : "Syntax error",
+    201 : "Invalid parameter",
+    205 : "Invalid user",
+    206 : "Domain name missing",
+    207 : "Already logged in",
+    208 : "Invalid username",
+    209 : "Invalid screen name",
+    210 : "User list full",
+    215 : "User already there",
+    216 : "User already on list",
+    217 : "User not online",
+    218 : "Already in mode",
+    219 : "User is in the opposite list",
+    223 : "Too many groups",
+    224 : "Invalid group",
+    225 : "User not in group",
+    229 : "Group name too long",
+    230 : "Cannot remove group 0",
+    231 : "Invalid group",
+    280 : "Switchboard failed",
+    281 : "Transfer to switchboard failed",
+
+    300 : "Required field missing",
+    301 : "Too many FND responses",
+    302 : "Not logged in",
+
+    500 : "Internal server error",
+    501 : "Database server error",
+    502 : "Command disabled",
+    510 : "File operation failed",
+    520 : "Memory allocation failed",
+    540 : "Wrong CHL value sent to server",
+
+    600 : "Server is busy",
+    601 : "Server is unavaliable",
+    602 : "Peer nameserver is down",
+    603 : "Database connection failed",
+    604 : "Server is going down",
+    605 : "Server unavailable",
+
+    707 : "Could not create connection",
+    710 : "Invalid CVR parameters",
+    711 : "Write is blocking",
+    712 : "Session is overloaded",
+    713 : "Too many active users",
+    714 : "Too many sessions",
+    715 : "Not expected",
+    717 : "Bad friend file",
+    731 : "Not expected",
+
+    800 : "Requests too rapid",
+
+    910 : "Server too busy",
+    911 : "Authentication failed",
+    912 : "Server too busy",
+    913 : "Not allowed when offline",
+    914 : "Server too busy",
+    915 : "Server too busy",
+    916 : "Server too busy",
+    917 : "Server too busy",
+    918 : "Server too busy",
+    919 : "Server too busy",
+    920 : "Not accepting new users",
+    921 : "Server too busy",
+    922 : "Server too busy",
+    923 : "No parent consent",
+    924 : "Passport account not yet verified"
+
+}
+
+# mapping of status codes to readable status format
+statusCodes = {
+
+    STATUS_ONLINE  : "Online",
+    STATUS_OFFLINE : "Offline",
+    STATUS_HIDDEN  : "Appear Offline",
+    STATUS_IDLE    : "Idle",
+    STATUS_AWAY    : "Away",
+    STATUS_BUSY    : "Busy",
+    STATUS_BRB     : "Be Right Back",
+    STATUS_PHONE   : "On the Phone",
+    STATUS_LUNCH   : "Out to Lunch"
+
+}
+
+# mapping of list ids to list codes
+listIDToCode = {
+
+    FORWARD_LIST : 'fl',
+    BLOCK_LIST   : 'bl',
+    ALLOW_LIST   : 'al',
+    REVERSE_LIST : 'rl'
+
+}
+
+# mapping of list codes to list ids
+listCodeToID = {}
+for id,code in listIDToCode.items():
+    listCodeToID[code] = id
+
+del id, code
diff --git a/src/tlib/proxy.py b/src/tlib/proxy.py
new file mode 100644 (file)
index 0000000..d48049d
--- /dev/null
@@ -0,0 +1,59 @@
+# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Licensed for distribution under the GPL version 2, check COPYING for details
+
+from twisted.internet.ssl import ClientContextFactory
+from twisted.internet.protocol import Protocol, ClientFactory
+from twisted.internet import reactor
+import utils # FLAG
+if(utils.checkTwisted()):
+       from twisted.web.http import HTTPClient
+else:
+       from twisted.protocols.http import HTTPClient
+
+
+class ProxyFactory(ClientFactory):
+       def __init__(self, server, serverport, otherfac):
+               self.server, self.serverport, self.otherfac = server, serverport, otherfac
+
+       def buildProtocol(self, addr):
+               p = ProxyProtocol()
+               p.server, p.serverport, p.otherfac = self.server, self.serverport, self.otherfac
+               return p
+
+class ProxyProtocol(HTTPClient):
+       def connectionMade(self):
+               self.ctxFactory = ClientContextFactory()
+               self.sendCommand("CONNECT", self.server + ":" + str(self.serverport))
+               self.endHeaders()
+               debug.log("proxy_connect_ssl: Sent CONNECT")
+       
+       def handleStatus(self, version, status, message):
+               if(status == "200"): # Excellent
+                       # Must flip over into SSL now
+                       debug.log("proxy_connect_ssl: Flipping into SSL mode")
+                       self.transport.startTLS(self.ctxFactory)
+                       p = self.otherfac.buildProtocol(self.transport.getPeer())
+                       p.makeConnection(self.transport)
+                       self.transport.protocol = p
+                       p.connectionMade()
+                       debug.log("proxy_connect_ssl: Flipped into SSL mode successfully. Handed over to real protocol.")
+               else:
+                       # Must cancel factory
+                       debug.log("proxy_connect_ssl: Error with proxy")
+                       self.otherfac.clientConnectionFailed(self.transport.connector, "Proxy connection failed")
+
+       def handleHeader(self, key, val):
+               pass
+       
+       def handleEndHeaders(self):
+               pass
+       
+       def handleResponse(self, r):
+               pass
+
+def proxy_connect_ssl(proxy, proxyport, server, serverport, factory):
+       debug.log("proxy_connect_ssl")
+       pfac = ProxyFactory(server, serverport, factory)
+       reactor.connectTCP(proxy, proxyport, pfac)
+
+
diff --git a/src/tlib/xmlstream.py b/src/tlib/xmlstream.py
new file mode 100644 (file)
index 0000000..bb7f1da
--- /dev/null
@@ -0,0 +1,223 @@
+# -*- test-case-name: twisted.test.test_xmlstream -*-
+#
+# Twisted, the Framework of Your Internet
+# Copyright (C) 2001 Matthew W. Lefkowitz
+# 
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of version 2.1 of the GNU Lesser General Public
+# License as published by the Free Software Foundation.
+# 
+# This library 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
+# Lesser General Public License for more details.
+# 
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from twisted.internet import reactor, protocol, defer
+from twisted.xish import utility
+import domish
+
+STREAM_CONNECTED_EVENT = intern("//event/stream/connected")
+STREAM_START_EVENT = intern("//event/stream/start")
+STREAM_END_EVENT = intern("//event/stream/end")
+STREAM_ERROR_EVENT = intern("//event/stream/error")
+STREAM_AUTHD_EVENT = intern("//event/stream/authd")
+RAWDATA_IN_EVENT = intern("//event/rawdata/in")
+RAWDATA_OUT_EVENT = intern("//event/rawdata/out")
+
+def hashPassword(sid, password):
+    """Create a SHA1-digest string of a session identifier and password """
+    import sha
+    return sha.new("%s%s" % (sid, password)).hexdigest()
+
+class Authenticator:
+    """ Base class for business logic of authenticating an XmlStream
+
+    Subclass this object to enable an XmlStream to authenticate to different
+    types of stream hosts (such as clients, components, etc.).
+
+    Rules:
+      1. The Authenticator MUST dispatch a L{STREAM_AUTHD_EVENT} when the stream
+         has been completely authenticated.
+      2. The Authenticator SHOULD reset all state information when
+         L{associateWithStream} is called.
+      3. The Authenticator SHOULD override L{streamStarted}, and start
+         authentication there.
+
+
+    @type namespace: C{str}
+    @cvar namespace: Default namespace for the XmlStream
+
+    @type version: C{int}
+    @cvar version: Version attribute for XmlStream. 0.0 will cause the
+                   XmlStream to not include a C{version} attribute in the
+                   header.
+
+    @type streamHost: C{str}
+    @ivar streamHost: Target host for this stream (used as the 'to' attribute)
+
+    @type xmlstream: C{XmlStream}
+    @ivar xmlstream: The XmlStream that needs authentication
+    """
+
+    namespace = 'invalid' # Default namespace for stream
+    version = 0.0         # Stream version
+
+    def __init__(self, streamHost):
+        self.streamHost = streamHost
+        self.xmlstream = None
+
+    def connectionMade(self):
+        """
+        Called by the XmlStream when the underlying socket connection is
+        in place. This allows the Authenticator to send an initial root
+        element, if it's connecting, or wait for an inbound root from
+        the peer if it's accepting the connection
+
+        Subclasses can use self.xmlstream.send() with the provided xmlstream
+        parameter to send any initial data to the peer
+        """
+
+    def streamStarted(self, rootelem):
+        """
+        Called by the XmlStream when it has received a root element from
+        the connected peer. 
+        
+        @type rootelem: C{Element}
+        @param rootelem: The root element of the XmlStream received from
+                         the streamHost
+        """
+
+    def associateWithStream(self, xmlstream):
+        """
+        Called by the XmlStreamFactory when a connection has been made
+        to the requested peer, and an XmlStream object has been
+        instantiated.
+
+        The default implementation just saves a handle to the new
+        XmlStream.
+
+        @type xmlstream: C{XmlStream}
+        @param xmlstream: The XmlStream that will be passing events to this
+                          Authenticator.
+        
+        """
+        self.xmlstream = xmlstream
+
+class ConnectAuthenticator(Authenticator):
+    def connectionMade(self):
+        # Generate stream header
+        if self.version == 1.0:
+            sh = "<stream:stream xmlns='%s' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>" % \
+                 (self.namespace)
+        else:
+            sh = "<stream:stream xmlns='%s' xmlns:stream='http://etherx.jabber.org/streams' to='%s'>" % \
+                 (self.namespace, self.streamHost)
+        self.xmlstream.send(sh)
+    
+class XmlStream(protocol.Protocol, utility.EventDispatcher):
+    def __init__(self, authenticator):
+        utility.EventDispatcher.__init__(self)
+        self.stream = None
+        self.authenticator = authenticator
+        self.sid = None
+        self.rawDataOutFn = None
+        self.rawDataInFn = None
+
+        # Reset the authenticator
+        authenticator.associateWithStream(self)
+
+        # Setup watcher for stream errors
+        self.addObserver("/error[@xmlns='http://etherx.jabber.org/streams']", self.streamError)
+
+    def streamError(self, errelem):
+        self.dispatch(errelem, STREAM_ERROR_EVENT)
+        self.transport.loseConnection()
+
+    ### --------------------------------------------------------------
+    ###
+    ### Protocol events
+    ###
+    ### --------------------------------------------------------------
+    def connectionMade(self):
+        # Setup the parser
+        self.stream = domish.elementStream()
+        self.stream.DocumentStartEvent = self.onDocumentStart
+        self.stream.ElementEvent = self.onElement
+        self.stream.DocumentEndEvent = self.onDocumentEnd
+
+        self.dispatch(self, STREAM_CONNECTED_EVENT)
+
+        self.authenticator.connectionMade()
+
+    def dataReceived(self, buf):
+        try:
+            if self.rawDataInFn: self.rawDataInFn(buf)
+            self.stream.parse(buf)
+        except domish.ParserError:
+            self.dispatch(self, STREAM_ERROR_EVENT)
+            self.transport.loseConnection()
+
+    def connectionLost(self, _):
+        self.dispatch(self, STREAM_END_EVENT)
+        self.stream = None
+        
+    ### --------------------------------------------------------------
+    ###
+    ### DOM events
+    ###
+    ### --------------------------------------------------------------
+    def onDocumentStart(self, rootelem):
+        if rootelem.hasAttribute("id"):
+            self.sid = rootelem["id"]                  # Extract stream identifier
+        self.authenticator.streamStarted(rootelem) # Notify authenticator
+        self.dispatch(self, STREAM_START_EVENT)    
+
+    def onElement(self, element):
+        self.dispatch(element)
+
+    def onDocumentEnd(self):
+        self.transport.loseConnection()
+
+    def setDispatchFn(self, fn):
+        self.stream.ElementEvent = fn
+
+    def resetDispatchFn(self):
+        self.stream.ElementEvent = self.onElement
+
+    def send(self, obj):
+        if isinstance(obj, domish.Element):
+            obj = obj.toXml()
+            
+        if self.rawDataOutFn:
+            self.rawDataOutFn(obj)
+            
+        self.transport.write(obj)
+
+
+class XmlStreamFactory(protocol.ReconnectingClientFactory):
+    def __init__(self, authenticator):
+        self.authenticator = authenticator
+        self.bootstraps = []
+
+    def buildProtocol(self, _):
+        self.resetDelay()
+        # Create the stream and register all the bootstrap observers
+        xs = XmlStream(self.authenticator)
+        xs.factory = self
+        for event, fn in self.bootstraps: xs.addObserver(event, fn)
+        return xs
+
+    def addBootstrap(self, event, fn):
+        self.bootstraps.append((event, fn))
+
+    def removeBootstrap(self, event, fn):
+        self.bootstraps.remove((event, fn))
+
+
+
+
+        
diff --git a/src/utils.py b/src/utils.py
new file mode 100644 (file)
index 0000000..fc29c99
--- /dev/null
@@ -0,0 +1,179 @@
+# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Licensed for distribution under the GPL version 2, check COPYING for details
+
+def fudgestr(text, num):
+       if(not (text.__class__ in [str, unicode])): return ""
+       newtext = ""
+       for c in text:
+               i = ord(c)
+               if(i >= num):
+                       i = ord(' ')
+               newtext += chr(i)
+       return newtext
+
+def latin1(text):
+       return fudgestr(text, 128)
+
+
+def copyDict(dic):
+       """ Does a deep copy of a dictionary """
+       out = {}
+       for key in dic.keys():
+               out[key] = dic[key]
+       return out
+
+def copyList(lst):
+       """ Does a deep copy of a list """
+       out = []
+       for i in lst:
+               out.append(i)
+       return out
+
+def mutilateMe(me):
+       """ Mutilates a class :) """
+#      for key in dir(me):
+#              exec "me." + key + " = None"
+
+def getLang(el):
+       return el.getAttribute((u'http://www.w3.org/XML/1998/namespace', u'lang'))
+
+
+errorCodeMap = {
+"bad-request"                  :       400,
+"conflict"                     :       409,
+"feature-not-implemented"      :       501,
+"forbidden"                    :       403,
+"gone"                         :       302,
+"internal-server-error"                :       500,
+"item-not-found"               :       404,
+"jid-malformed"                        :       400,
+"not-acceptable"               :       406,
+"not-allowed"                  :       405,
+"not-authorized"               :       401,
+"payment-required"             :       402,
+"recipient-unavailable"                :       404,
+"redirect"                     :       302,
+"registration-required"                :       407,
+"remote-server-not-found"      :       404,
+"remote-server-timeout"                :       504,
+"resource-constraint"          :       500,
+"service-unavailable"          :       503,
+"subscription-required"                :       407,
+"undefined-condition"          :       500,
+"unexpected-request"           :       400
+}
+
+def doPath(path):
+       if(path and path[0] == "/"):
+               return path
+       else:
+               return "../" + path
+
+
+def parseText(text):
+       t = TextParser()
+       t.parseString(text)
+       return t.root
+
+def parseFile(filename):
+       t = TextParser()
+       t.parseFile(filename)
+       return t.root
+
+class TextParser:
+       """ Taken from http://xoomer.virgilio.it/dialtone/rsschannel.py """
+
+       def __init__(self):
+               self.root = None
+
+       def parseFile(self, filename):
+               return self.parseString(file(filename).read())
+
+       def parseString(self, data):
+               if(checkTwisted()):
+                       from twisted.xish.domish import SuxElementStream
+               else:
+                       from tlib.domish import SuxElementStream
+               es = SuxElementStream()
+               es.DocumentStartEvent = self.docStart
+               es.DocumentEndEvent = self.docEnd
+               es.ElementEvent = self.element
+               es.parse(data)
+               return self.root
+
+       def docStart(self, e):
+               self.root = e
+
+       def docEnd(self):
+               pass
+
+       def element(self, e):
+               self.root.addChild(e)
+
+
+
+checkTwistedCached = None
+def checkTwisted():
+       """ Returns False if we're using an old version that needs tlib, otherwise returns True """
+       global checkTwistedCached
+       if(checkTwistedCached == None):
+               import twisted.copyright
+               checkTwistedCached = (VersionNumber(twisted.copyright.version) >= VersionNumber("2.0.0"))
+       return checkTwistedCached
+
+class VersionNumber:
+       def __init__(self, vstring):
+               self.varray = [0]
+               index = 0 
+               flag = True
+               for c in vstring:
+                       if(c == '.'):
+                               self.varray.append(0)
+                               index += 1
+                               flag = True
+                       elif(c.isdigit() and flag):
+                               self.varray[index] *= 10
+                               self.varray[index] += int(c)
+                       else:
+                               flag = False
+       
+       def __cmp__(self, other):
+               i = 0
+               while(True):
+                       if(i == len(other.varray)):
+                               if(i < len(self.varray)):
+                                       return 1
+                               else:
+                                       return 0
+                       if(i == len(self.varray)):
+                               if(i < len(other.varray)):
+                                       return -1
+                               else:
+                                       return 0
+
+                       if(self.varray[i] > other.varray[i]):
+                               return 1
+                       elif(self.varray[i] < other.varray[i]):
+                               return -1
+
+                       i += 1
+       
+
+
+class RollingStack:
+       def __init__(self, size):
+               self.lst = []
+               self.size = size
+       
+       def push(self, data):
+               self.lst.append(str(data))
+               if(len(self.lst) > self.size):
+                       self.lst.remove(self.lst[0])
+       
+       def grabAll(self):
+               return "".join(self.lst)
+       
+       def flush(self):
+               self.lst = []
+
+
diff --git a/src/xdb.py b/src/xdb.py
new file mode 100644 (file)
index 0000000..98cdfad
--- /dev/null
@@ -0,0 +1,96 @@
+# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Licensed for distribution under the GPL version 2, check COPYING for details
+
+import utils
+if(utils.checkTwisted()):
+       from twisted.xish.domish import Element
+else:
+       from tlib.domish import Element
+
+import os
+import os.path
+import debug
+import config
+import legacy
+
+SPOOL_UMASK = 0177
+
+class XDB:
+       """
+       Class for storage of data. Compatible with xdb_file from Jabberd1.4.x
+       Allows PyMSN-t to be compatible with MSN-t
+       
+       Create one instance of the class for each XDB 'folder' you want.
+       Call request()/set() with the xdbns argument you wish to retrieve
+       """
+       def __init__(self, name, mangle=False):
+               """ Creates an XDB object. If mangle is True then any '@' signs in filenames will be changed to '%' """
+               self.name = utils.doPath(config.spooldir) + '/' + name
+               if not os.path.exists(self.name) :
+                       os.makedirs(self.name)
+               self.mangle = mangle
+       
+       def __getFile(self, file):
+               if(self.mangle):
+                       file = file.replace('@', '%')
+               
+               document = utils.parseFile(self.name + "/" + file + ".xml")
+               
+               return document
+       
+       def __writeFile(self, file, text):
+               if(self.mangle):
+                       file = file.replace('@', '%')
+               
+               prev_umask = os.umask(SPOOL_UMASK)
+               f = open(self.name + "/" + file + ".xml", "w")
+               f.write(text)
+               f.close()
+               os.umask(prev_umask)
+       
+       
+       def request(self, file, xdbns):
+               """ Requests a specific xdb namespace from the XDB 'file' """
+               try:
+                       document = self.__getFile(file)
+                       for child in document.elements():
+                               if(child.getAttribute("xdbns") == xdbns):
+                                       return child
+               except:
+                       return None
+       
+       def set(self, file, xdbns, element):
+               """ Sets a specific xdb namespace in the XDB 'file' to element """
+               try:
+                       element.attributes["xdbns"] = xdbns
+                       document = None
+                       try:
+                               document = self.__getFile(file)
+                       except IOError:
+                               pass
+                       if(not document):
+                               document = Element((None, "xdb"))
+                       
+                       # Remove the existing node (if any)
+                       for child in document.elements():
+                               if(child.getAttribute("xdbns") == xdbns):
+                                       document.children.remove(child)
+                       # Add the new one
+                       document.addChild(element)
+                       
+                       self.__writeFile(file, document.toXml())
+               except:
+                       debug.log("XDB error writing entry %s to file %s" % (xdbns, file))
+                       raise
+       
+       def remove(self, file):
+               """ Removes an XDB file """
+               file = self.name + "/" + file + ".xml"
+               if(self.mangle):
+                       file = file.replace('@', '%')
+               try:
+                       os.remove(file)
+               except:
+                       debug.log("XDB error removing file " + file)
+                       raise
+
diff --git a/src/xmlconfig.py b/src/xmlconfig.py
new file mode 100644 (file)
index 0000000..b630d95
--- /dev/null
@@ -0,0 +1,50 @@
+# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Licensed for distribution under the GPL version 2, check COPYING for details
+
+
+import sys
+import os
+
+import utils
+import config
+
+
+def invalidError(text):
+       print text
+       print "Exiting..."
+       sys.exit(1)
+
+
+def reloadConfig():
+       # Find out where the config file is
+       configFile = "../config.xml"
+       if(len(sys.argv) == 2):
+               configFile = sys.argv[1]
+
+       # Check the file exists
+       if(not os.path.isfile(configFile)):
+               print "Configuration file not found. You need to create a config.xml file in the PyMSNt directory."
+               sys.exit(1)
+
+       # Get ourself a DOM
+       root = utils.parseFile(configFile)
+
+       # Store all the options in config
+       for el in root.elements():
+               try:
+                       tag = el.name
+                       cdata = str(el)
+                       if(cdata):
+                               # For config options like <ip>127.0.0.1</ip>
+                               if(type(getattr(config, tag)) != str):
+                                       invalidError("Tag %s in your configuration file should be a boolean (ie, no cdata)." % (tag))
+                               setattr(config, tag, cdata)
+                       else:
+                               # For config options like <sessionGreeting/>
+                               if(type(getattr(config, tag)) not in [bool, int]):
+                                       invalidError("Tag %s in your configuration file should be a string (ie, must have cdata)." % (tag))
+                               setattr(config, tag, True)
+               except AttributeError:
+                       print "Tag %s in your configuration file is not a defined tag. Ignoring!" % (tag)
+
+