]> code.delx.au - pymsnt/commitdiff
Reimport and tags (0.10.1)
authorjamesbunton <jamesbunton@55fbd22a-6204-0410-b2f0-b6c764c7e90a>
Mon, 31 Oct 2005 01:09:16 +0000 (01:09 +0000)
committerjamesbunton <jamesbunton@55fbd22a-6204-0410-b2f0-b6c764c7e90a>
Mon, 31 Oct 2005 01:09:16 +0000 (01:09 +0000)
git-svn-id: http://delx.cjb.net/svn/pymsnt/trunk@3 55fbd22a-6204-0410-b2f0-b6c764c7e90a

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

30 files changed:
PyMSNt
PyMSNt.tac [new file with mode: 0644]
README
TODO
config-example.xml
src/avatar.py [new file with mode: 0644]
src/baseproto/__init__.py
src/baseproto/glue.py
src/config.py
src/contact.py [new file with mode: 0644]
src/debug.py
src/disco.py
src/groupchat.py
src/housekeep.py [new file with mode: 0644]
src/jabw.py
src/lang.py
src/legacy/__init__.py
src/legacy/defaultAvatar.png [new file with mode: 0644]
src/legacy/defaultJabberAvatar.png [new file with mode: 0644]
src/legacy/glue.py
src/legacy/legacylist.py [new file with mode: 0644]
src/legacy/msnw.py
src/main.py
src/misciq.py
src/register.py
src/session.py
src/tlib/msn.py
src/utils.py
src/xdb.py
src/xmlconfig.py

diff --git a/PyMSNt b/PyMSNt
index a33c1719edc88641001805f8e84dc5b091f4c3f7..d73cc9d74382a9f1b819766ee4feb83af44c9f51 100755 (executable)
--- a/PyMSNt
+++ b/PyMSNt
@@ -1,6 +1,7 @@
 #!/bin/bash
 
-cd `dirname $0`/src
-exec -a PyMSNt python main.py
-cd `dirname $0`
+exec -a PyMSNt python src/main.py $*
 
+# Comment out the above line and use the below for twistd
+#PPATH="/usr/local/PyMSNt"
+#twistd -oy "$PPATH/PyMSNt.tac" -r poll --pidfile "$PPATH/PyMSNt.pid" -l "$PPATH/debug.log"
diff --git a/PyMSNt.tac b/PyMSNt.tac
new file mode 100644 (file)
index 0000000..e41586a
--- /dev/null
@@ -0,0 +1,25 @@
+# Path to the PyMSNt installed directory
+PATH = "/usr/local/PyMSNt/"
+
+# Path to the configuration file
+CONFIG = "/usr/local/PyMSNt/config.xml"
+
+####
+# You shouldn't need to modify below this line
+####
+
+
+# Make 'cwd'/src in the PYTHONPATH
+import sys
+import os
+import os.path
+sys.path[0] = os.path.abspath(PATH + "/src/")
+os.chdir(PATH)
+
+# Set up the service
+import main
+from twisted.application import service
+application = service.Application("PyMSNt")
+service = main.App()
+service.c.setServiceParent(application)
+
diff --git a/README b/README
index af645814887563f7984dfaa246e1089b8b3cb16f..2f767b583f2c3e59c4d6568d5a3f1b2890cc458d 100644 (file)
--- a/README
+++ b/README
@@ -1,11 +1,24 @@
 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 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.
+If you want more control over daemonisation and logging, have a look at
+the twistd manpage. Edit PyMSNt.tac and run with twistd.
+Examples:
+To start as a daemon run:
+# twistd -oy PyMSNt.tac
+To start as a daemon, with logging, a PID file and poll as the reactor:
+# twistd -oy PyMSNt.tac -r poll --pidfile /var/run/PyMSNt.pid -l /var/log/PyMSNt.log
+
+
+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.
+* 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
index eb1ba770afedfc0ea5951c2cdc094c23b1141c8a..20a1509540333ccfa17674785580d4da99025f3e 100644 (file)
--- a/TODO
+++ b/TODO
@@ -1,35 +1,15 @@
-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.9.4:
+* Fix any outstanding bugs.
+* Hopefully get all the translations up to scratch
 
 
-For 0.10 - The I Want This Right Now release:
-* File transfer (JEP0096)
-* ACL support
+For 0.10:
+* Load testing.
+* Check avatar compatibility with various MSN plugins.
+* Handle timeouts connecting to the MSN dispatch server as errors.
+* Get all translations to remove references to nicknames from registerText
+* Decide on default for legacy.msnw.GETALLAVATARS
 
-
-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 :)
+For 0.11:
+* File transfer (new & old)
 
index c495fd8b8852b5d5a8eea106dd23c20f9e7b7916..31b7b5f20f29be2653eaabdf5106f4930aa7291f 100644 (file)
@@ -4,15 +4,14 @@
 
 <!-- The JabberID of the transport -->
 <jid>msn</jid>
+<!-- The component JID of the transport. Unless you're doing clustering, leave this alone -->
+<!-- <compjid>msn1</compjid> -->
 
 
 <!-- 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>
@@ -33,42 +32,20 @@ Do not include the jid of the transport -->
 <!-- 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>
-
+<!-- Get all avatars. If this is set to true then avatars are grabbed for all your contacts immediately. If false then avatars are only grabbed when you're in a chat with a contact -->
+<getAllAvatars/>
+<!-- Use Jabber.com's XCP component protocol extensions. --> 
+<!-- <useXCP/> -->
+
+<!-- You can choose which users you wish to have as administrators. These users can perform some tasks with Ad-Hoc commands that others cannot -->
+<!--<admins>
+<jid>admin@host.com</jid>
+</admins>-->
 
 </pymsnt>
diff --git a/src/avatar.py b/src/avatar.py
new file mode 100644 (file)
index 0000000..3912450
--- /dev/null
@@ -0,0 +1,123 @@
+# Copyright 2005 James Bunton <james@delx.cjb.net>
+# Licensed for distribution under the GPL version 2, check COPYING for details
+
+import utils
+import config
+from twisted.internet import reactor
+if(utils.checkTwisted()):
+       from twisted.xish.domish import Element
+else:
+       from tlib.domish import Element
+from debug import LogEvent, INFO, WARN, ERROR
+import jabw
+import config
+import lang
+import sha
+import base64
+import os
+import os.path
+
+SPOOL_UMASK = 0077
+
+def parsePhotoEl(photo):
+       """ Pass the photo element as an avatar, returns the avatar imageData """
+       imageData = ""
+       imageType = ""
+       for e in photo.elements():
+               if(e.name == "BINVAL"):
+                       imageData = base64.decodestring(e.__str__())
+               elif(e.name == "TYPE"):
+                       imageType = e.__str__()
+       
+       if(imageType != "image/png"):
+               imageData = utils.convertToPNG(imageData)
+       
+       return imageData
+
+
+
+class Avatar:
+       """ Represents an Avatar. Does not store the image in memory. """
+       def __init__(self, imageData, avatarCache):
+               self.__imageHash = sha.sha(imageData).hexdigest()
+               self.__avatarCache = avatarCache
+
+       def getImageHash(self):
+               """ Returns the SHA1 hash of the avatar. """
+               return self.__imageHash
+       
+       def getImageData(self):
+               """ Returns this Avatar's imageData. This loads data from a file. """
+               return self.__avatarCache.getAvatarData(self.__imageHash)
+       
+       def makePhotoElement(self):
+               """ Returns an XML Element that can be put into the vCard. """
+               photo = Element((None, "PHOTO"))
+               type = photo.addElement("TYPE")
+               type.addContent("image/png")
+               binval = photo.addElement("BINVAL")
+               binval.addContent(base64.encodestring(self.getImageData()))
+               return photo
+
+       def makeDataElement(self):
+               """ Returns an XML Element that can be put into a jabber:x:avatar IQ stanza. """
+               data = Element((None, "data"))
+               data["mimetype"] = "image/png"
+               data.addContent(base64.encodestring(self.getImageData()))
+               return data
+
+       def __eq__(self, other):
+               return (other and self.__imageHash == other.__imageHash)
+
+
+class AvatarCache:
+       """ Manages avatars on disk. Avatars are stored according to their SHA1 hash.
+       The layout is config.spooldir / config.jid / avatars / "first two characters of SHA1 hash" """
+
+       def dir(self, key):
+               """ Returns the full path to the directory that a 
+               particular key is in. Creates that directory if it doesn't already exist. """
+               d = os.path.abspath(config.spooldir) + "/" + config.jid + "/avatars/" + key[0:3] + "/"
+               if not os.path.exists(d):
+                       os.makedirs(d)
+               return d
+       
+       def setAvatar(self, imageData):
+               """ Writes an avatar to disk according to its key.
+               Returns an Avatar object. """
+               avatar = Avatar(imageData, self)
+               key = avatar.getImageHash()
+               LogEvent(INFO, "", "Setting avatar %s" % (key))
+               prev_umask = os.umask(SPOOL_UMASK)
+               try:
+                       f = open(self.dir(key) + key, 'w')
+                       f.write(imageData)
+                       f.close()
+               except IOError, e:
+                       LogEvent(WARN, "", "IOError writing to avatar %s - %s" % (key, str(e)))
+               os.umask(prev_umask)
+               return avatar
+       
+       def getAvatar(self, key):
+               """ Loads the avatar with SHA1 hash of 'key' from disk and returns an Avatar object """
+               imageData = self.getAvatarData(key)
+               if imageData:
+                       return Avatar(imageData, self)
+       
+       def getAvatarData(self, key):
+               """ Loads the avatar with SHA1 hash of 'key' from disk and returns the data """
+               try:
+                       filename = self.dir(key) + key
+                       if os.path.isfile(filename):
+                               LogEvent(INFO, "Getting avatar.")
+                               f = open(filename)
+                               data = f.read()
+                               f.close()
+                               return data
+                       else:
+                               LogEvent(INFO, "", "Avatar not found.")
+               except IOError, e:
+                       LogEvent(WARN, "", "IOError reading avatar.")
+
+
+
index df8b9dd59eaa3a30e9b5bf21858de8cad0acf0f0..9b9dd6af3741cab3f0222a60b8c9a13d2db1ce48 100644 (file)
@@ -1,3 +1,3 @@
-from glue import LegacyConnection, LegacyGroupchat, translateAccount
-from glue import name, version, mangle, id, namespace
+from glue import LegacyConnection, LegacyGroupchat, translateAccount, startStats, updateStats
+from glue import name, url, version, mangle, id, namespace
 from glue import formRegEntry, getAttributes, isGroupJID
index d273dfc9bf917be4154a65c34608e243f0995748..062ce51a8601b622416ac9010e3cf7a9a53cbe1c 100644 (file)
@@ -1,12 +1,24 @@
-# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Copyright 2005 James Bunton <james@delx.cjb.net>
 # Licensed for distribution under the GPL version 2, check COPYING for details
 
-from tlib.domish import Element
+import utils
+if(utils.checkTwisted()):
+       from twisted.xish.domish import Element
+else:
+       from tlib.domish import Element
+import avatar
 import groupchat
+import config
+import debug
+import lang
+
 
 # The name of the transport
 name = "Foo Transport"
 
+# The URL of the transport's home page
+url = "http://foo.jabberstudio.org"
+
 # The transport version
 version = "0.1"
 
@@ -34,10 +46,10 @@ def formRegEntry(username, password, nickname):
 
 
 def getAttributes(base):
-       """ This function should, given a spool domish.Element, pull the username, password,
-       and nickname out of it and return them """
+       """ This function should, given a spool domish.Element, pull the username and password
+       out of it and return them """
        pass
-#      return username, password, nickname
+#      return username, password
 
 
 
@@ -47,6 +59,19 @@ def translateAccount(legacyaccount):
        pass
 
 
+def startStats(statistics):
+       """ Fills the misciq.Statistics class with the statistics fields.
+       You must put a command_OnlineUsers and command_OnlineUsers_Desc
+       attributes into the lang classes for this to work.
+       Note that OnlineUsers is a builtin stat. You don't need to
+       reimplement it yourself. """
+       #statistics.stats["OnlineUsers"] = 0
+       pass
+
+def updateStats(statistics):
+       """ This will get called regularly. Use it to update any global
+       statistics """
+       pass
 
 class LegacyGroupchat(groupchat.BaseGroupchat):
        """ A class to represent a groupchat on the legacy service. All the functions below
@@ -69,6 +94,31 @@ class LegacyGroupchat(groupchat.BaseGroupchat):
 
 
 
+class LegacyList:
+       """ A base class that must have all functions reimplemented by legacy protocol to allow
+       legacy contact list to be accessible and modifiable from Jabber """
+       def __init__(self, session):
+               self.session = session
+       
+       def removeMe(self):
+               self.session = None
+       
+       def addContact(self, jid):
+               """ Must add this JID to the legacy list """
+               pass
+       
+       def authContact(self, jid):
+               """ Must authorise this JID on the legacy service """
+               pass
+       
+       def removeContact(self, jid):
+               """ Must remove this JID from the legacy list """
+               pass
+       
+       def deauthContact(self, jid):
+               """ Must deauthorise this JID on the legacy service """
+               pass
+
 
 class LegacyConnection:
        """ A base class that must have all functions reimplemented by legacy protocols to translate
@@ -77,13 +127,15 @@ class LegacyConnection:
        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
+               self.session = session
+               self.legacyList = LegacyList()
        
        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
+               Must cleanly end the user's legacy protocol session and delete
+               this object. """
+               self.session = None
+               self.legacyList = None
        
        def resourceOffline(self, resource):
                """ Called whenever one of the local user's resources goes offline """
@@ -93,17 +145,14 @@ class LegacyConnection:
                """ Called whenever PyTransport wants to send a message to a remote user """
                pass
        
-       def setStatus(self, show, friendly):
+       def setStatus(self, nickname, show, status):
                """ 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 """
+               'nickname' is the Jabber nickname, 'show' is a Jabber status description, and status
+               is a personal message describing the user's current status/activities """
                pass
        
-       def jabberSubscriptionReceived(self, to, subtype):
-               """ Called by PyTransport whenever a Jabber subscription packet is received """
+       def updateAvatar(self, av=None):
+               """ Called whenever a new avatar needs to be set. Instance of avatar.Avatar is passed """
                pass
        
        def userTypingNotification(self, dest, composing):
index 90901724bac7995aef9e5c85ea3d1f7c71315ba1..61d4e7149b1a6b62bc98aed288d9719536e209bf 100644 (file)
@@ -2,8 +2,8 @@
 # Please edit config.xml instead of this file
 
 jid = "msn"
+compjid = ""
 spooldir = ""
-pid = "PyMSNt.pid"
 
 mainServer = "127.0.0.1"
 mainServerJID = ""
@@ -14,18 +14,11 @@ secret = "secret"
 lang = "en"
 
 mailNotifications = False
-fancyFriendly = False
 sessionGreeting = ""
 registerMessage = ""
 allowRegister = False
+getAllAvatars = False
+useXCP = False
 
-reactor = ""
-
-proxyServer = ""
-proxyPort = ""
-
-debugOn = False
-debugSmart = False
-debugLog = ""
-
+admins = []
 
diff --git a/src/contact.py b/src/contact.py
new file mode 100644 (file)
index 0000000..48cb184
--- /dev/null
@@ -0,0 +1,225 @@
+# Copyright 2005 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
+from debug import LogEvent, INFO, WARN, ERROR
+import disco
+import legacy
+import jabw
+import config
+import lang
+import sha
+
+
+class Contact:
+       """ Represents a Jabber contact """
+       def __init__(self, jid, sub, contactList):
+               self.jid = jid
+               self.contactList = contactList
+               self.groups = []
+               self.sub = sub
+               self.nickname = ""
+               self.avatar = None
+               self.show = ""
+               self.status = ""
+               self.ptype = "unavailable"
+       
+       def removeMe(self):
+               """ Destroys this object. Does not remove the contact from the server's list. """
+               self.contactList = None
+               self.avatar = None
+       
+       def syncContactGrantedAuth(self):
+               """ Since last using the transport the user has been granted authorisation by this contact.
+               Call this to synchronise the user's Jabber list with their legacy list after logon. """
+               if(self.sub == "none"):
+                       self.sub = "to"
+               elif(self.sub == "from"):
+                       self.sub = "both"
+               else:
+                       return
+               self.updateRoster("subscribe")
+
+       def syncContactRemovedAuth(self):
+               """ Since last using the transport the user has been blocked by this contact.
+               Call this to synchronise the user's Jabber list with their legacy list after logon. """
+               if(self.sub == "to"):
+                       self.sub = "none"
+               elif(self.sub == "both"):
+                       self.sub = "from"
+               else:
+                       return
+               self.updateRoster("unsubscribed")
+       
+       def syncUserGrantedAuth(self):
+               """ Since last using the transport the user has granted authorisation to this contact.
+               Call this to synchronise the user's Jabber list with their legacy list after logon. """
+               if(self.sub == "none"):
+                       self.sub = "from"
+               elif(self.sub == "to"):
+                       self.sub = "both"
+               else:
+                       return
+               self.updateRoster("subscribe")
+       
+       def syncUserRemovedAuth(self):
+               """ Since last using the transport the user has removed this contact's authorisation.
+               Call this to synchronise the user's Jabber list with their legacy list after logon. """
+               if(self.sub == "from"):
+                       self.sub = "none"
+               elif(self.sub == "both"):
+                       self.sub = "to"
+               else:
+                       return
+               self.updateRoster("unsubscribe")
+       
+       def syncGroups(self, groups, push=True):
+               """ Set the groups that this contact is in on the legacy service.
+               By default this pushes the groups out with a presence subscribed packet. """
+               self.groups = groups
+               if push: self.updateRoster("subscribed");
+       
+       def contactGrantsAuth(self):
+               """ Live roster event """
+               if(self.sub == "none"):
+                       self.sub = "to"
+               elif(self.sub == "from"):
+                       self.sub = "both"
+               self.sendSub("subscribed")
+               self.sendPresence()
+       
+       def contactRemovesAuth(self):
+               """ Live roster event """
+               if(self.sub == "to"):
+                       self.sub = "none"
+               elif(self.sub == "both"):
+                       self.sub = "from"
+               self.sendSub("unsubscribed")
+       
+       def contactRequestsAuth(self):
+               """ Live roster event """
+               self.sendSub("subscribe")
+       
+       def contactDerequestsAuth(self):
+               """ Live roster event """
+               self.sendSub("unsubscribe")
+       
+       def jabberSubscriptionReceived(self, subtype):
+               """ Updates the subscription state internally and pushes the update to the legacy server """
+               if subtype == "subscribe":
+                       if self.sub == "to" or self.sub == "both":
+                               self.sendSub("subscribed")
+                       self.contactList.legacyList.addContact(self.jid)
+
+               elif subtype == "subscribed":
+                       if self.sub == "none":
+                               self.sub = "from"
+                       if self.sub == "to":
+                               self.sub = "both"
+                       self.contactList.legacyList.authContact(self.jid)
+
+               elif subtype == "unsubscribe":
+                       if self.sub == "none" or self.sub == "from":
+                               self.sendSub("unsubscribed")
+                       if self.sub == "both":
+                               self.sub = "from"
+                       if self.sub == "to":
+                               self.sub = "none"
+                       self.contactList.legacyList.removeContact(self.jid)
+
+               elif(subtype == "unsubscribed"):
+                       if(self.sub == "both"):
+                               self.sub = "to"
+                       if(self.sub == "from"):
+                               self.sub = "none"
+                       self.contactList.legacyList.deauthContact(self.jid)
+
+       def updateNickname(self, nickname, push=True):
+               if(self.nickname != nickname):
+                       self.nickname = nickname
+                       if(push): self.sendPresence()
+       
+       def updatePresence(self, show, status, ptype, force=False):
+               updateFlag = (self.show != show or self.status != status or self.ptype != ptype or force)
+               self.show = show
+               self.status = status
+               self.ptype = ptype
+               if(updateFlag):
+                       self.sendPresence()
+       
+       def updateAvatar(self, avatar=None, push=True):
+               if(self.avatar == avatar): return
+               self.avatar = avatar
+               if(push): self.sendPresence()
+       
+       def sendSub(self, ptype):
+               self.contactList.session.sendPresence(to=self.contactList.session.jabberID, fro=self.jid, ptype=ptype)
+       
+       def sendPresence(self, tojid=""):
+               avatarHash = ""
+               if(self.avatar):
+                       avatarHash = self.avatar.getImageHash()
+               caps = Element((None, "c"))
+               caps.attributes["xmlns"] = disco.CAPS
+               caps.attributes["node"] = legacy.url + "/protocol/caps"
+               caps.attributes["ver"] = legacy.version
+               if not tojid:
+                       tojid=self.contactList.session.jabberID
+               self.contactList.session.sendPresence(to=tojid, fro=self.jid, ptype=self.ptype, show=self.show, status=self.status, avatarHash=avatarHash, nickname=self.nickname, payload=[caps])
+       
+       def updateRoster(self, ptype):
+               self.contactList.session.sendRosterImport(jid=self.jid, ptype=ptype, sub=self.sub, groups=self.groups, name=self.nickname)
+
+
+class ContactList:
+       """ Represents the Jabber contact list """
+       def __init__(self, session):
+               LogEvent(INFO, session.jabberID)
+               self.session = session
+               self.contacts = {}
+       
+       def removeMe(self):
+               """ Cleanly removes the object """
+               LogEvent(INFO, self.session.jabberID)
+               for jid in self.contacts:
+                       self.contacts[jid].updatePresence("", "", "unavailable")
+                       self.contacts[jid].removeMe()
+               self.contacts = {}
+               self.session = None
+               self.legacyList = None
+       
+       def resendLists(self, tojid=""):
+               for jid in self.contacts:
+                       if(self.contacts[jid].status != "unavailable"):
+                               self.contacts[jid].sendPresence(tojid)
+               LogEvent(INFO, self.session.jabberID)
+       
+       def createContact(self, jid, sub):
+               """ Creates a contact object. Use this to initialise the contact list
+               Returns a Contact object which you can call sync* methods on to synchronise
+               the user's legacy contact list with their Jabber list """
+               LogEvent(INFO, self.session.jabberID)
+               c = Contact(jid, sub, self)
+               self.contacts[jid] = c
+               return c
+       
+       def getContact(self, jid):
+               """ Finds the contact. If one doesn't exist then a new one is created, with sub set to "none" """
+               if(not self.contacts.has_key(jid)):
+                       self.contacts[jid] = Contact(jid, "none", self)
+               return self.contacts[jid]
+       
+       def findContact(self, jid):
+               if(self.contacts.has_key(jid)):
+                       return self.contacts[jid]
+               return None
+       
+       def jabberSubscriptionReceived(self, jid, subtype):
+               self.getContact(jid).jabberSubscriptionReceived(subtype)
+       
+
index d284ee9da0ef81090d9da1bf334a8e14c2f7e15d..780f27a665ef1f8503e8c76e547d74d48b734753 100644 (file)
@@ -1,76 +1,37 @@
-# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Copyright 2004-2005 James Bunton <james@delx.cjb.net>
 # Licensed for distribution under the GPL version 2, check COPYING for details
 
-import os
+from twisted.python import log
 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)
 
+class INFO : pass
+class WARN : pass
+class ERROR: pass
+
+class LogEvent:
+       def __init__(self, category=INFO, ident="", msg="", log=True):
+               self.category, self.ident, self.msg = category, ident, msg
+               frame = sys._getframe(1)
+               self.klass = frame.f_locals.get("self", frame.f_code.co_filename)
+               self.method = frame.f_code.co_name
+               self.args = frame.f_locals
+               if log:
+                       self.log()
+       
+       def __str__(self):
+               args = {}
+               for key in self.args.keys():
+                       val = self.args[key]
+                       args[key] = val
+                       try:
+                               if len(val) > 128:
+                                       args[key] = "Oversize arg"
+                       except:
+                               # If its not an object with length, assume that it can't be too big. Hope that's a good assumption.
+                               pass
+               category = str(self.category).split(".")[1]
+               return "%s :: %s :: %s :: %s :: %s :: %s" % (category, str(self.ident), self.msg, self.method, str(self.klass), str(args))
+       
+       def log(self):
+               log.msg(self)
 
index 000677e30854046ef43559ff73cc1644c4431402..a498fc7c027c9a8ef238b34c5c4dc42250468db3 100644 (file)
@@ -1,33 +1,63 @@
-# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Copyright 2004-2005 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.defer import Deferred
 from twisted.internet import reactor
+from debug import LogEvent, INFO, WARN, ERROR
 import sys
 import config
-import debug
 import legacy
+import lang
+
+XMPP_STANZAS  = "urn:ietf:params:xml:ns:xmpp-stanzas"
+DISCO         = "http://jabber.org/protocol/disco"
+DISCO_ITEMS   = DISCO + "#items"
+DISCO_INFO    = DISCO + "#info"
+COMMANDS      = "http://jabber.org/protocol/commands"
+CAPS          = "http://jabber.org/protocol/caps"
+SUBSYNC       = "http://jabber.org/protocol/roster-subsync"
+MUC           = "http://jabber.org/protocol/muc"
+MUC_USER      = MUC + "#user"
+IQGATEWAY     = "jabber:iq:gateway"
+IQVERSION     = "jabber:iq:version"
+IQREGISTER    = "jabber:iq:register"
+IQROSTER      = "jabber:iq:roster"
+IQAVATAR      = "jabber:iq:avatar"
+XCONFERENCE   = "jabber:x:conference"
+XEVENT        = "jabber:x:event"
+XDELAY        = "jabber:x:delay"
+XAVATAR       = "jabber:x:avatar"
+STORAGEAVATAR = "storage:client:avatar"
+XVCARDUPDATE  = "vcard-temp:x:update"
+VCARDTEMP     = "vcard-temp"
+
 
-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:
+       """ Handles everything IQ related. You can send IQ stanzas and receive a Deferred
+       to notify you when a response comes, or if there's a timeout.
+       Also manages discovery for server & client """
+
+       # TODO rename this file & class to something more sensible
+
        def __init__ (self, pytrans):
-               debug.log("Discovery: Created server discovery manager")
+               LogEvent(INFO)
                self.pytrans = pytrans
-               self.identities = []
-               self.features = []
+               self.identities = {}
+               self.features = {}
+               self.nodes = {}
                self.deferredIqs = {} # A dict indexed by (jid, id) of deferreds to fire
                
-               self.addFeature(DISCO, None)
+               self.addFeature(DISCO, None, config.jid)
+               self.addFeature(DISCO, None, "USER")
        
        def sendIq(self, el, timeout=15):
                """ Used for sending IQ packets.
@@ -49,123 +79,150 @@ class ServerDiscovery:
                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 addIdentity(self, category, ctype, name, jid):
+               """ Adds an identity to this JID's discovery profile. If jid == "USER" then MSN users will get this identity. """
+               LogEvent(INFO)
+               if not self.identities.has_key(jid):
+                       self.identities[jid] = []
+               self.identities[jid].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 addFeature(self, var, handler, jid):
+               """ Adds a feature to this JID's discovery profile. If jid == "USER" then MSN users will get this feature. """
+               LogEvent(INFO)
+               if not self.features.has_key(jid):
+                       self.features[jid] = []
+               self.features[jid].append((var, handler))
+
+       def addNode(self, node, handler, name, jid, rootnode):
+               """ Adds a node to this JID's discovery profile. If jid == "USER" then MSN users will get this node. """
+               LogEvent(INFO)
+               if not self.nodes.has_key(jid):
+                       self.nodes[jid] = {}
+               self.nodes[jid][node] = (handler, name, rootnode)
        
        def onIq(self, el):
+               """ Decides what to do with an IQ """
                fro = el.getAttribute("from")
                to = el.getAttribute("to")
                ID = el.getAttribute("id")
                iqType = el.getAttribute("type")
+               ulang = utils.getLang(el)
+               try: # Stringprep
+                       froj = jid.JID(fro)
+                       to = jid.JID(to).full()
+               except Exception, e:
+                       LogEvent(WARN, "", "Dropping IQ because of stringprep error")
 
                # Check if it's a response to a send IQ
-               if(self.deferredIqs.has_key((fro, ID)) and iqType in ["error", "result"]):
+               if self.deferredIqs.has_key((fro, ID)) and (iqType == "error" or iqType == "result"):
+                       LogEvent(INFO, "", "Doing callback")
                        self.deferredIqs[(fro, ID)].callback(el)
                        del self.deferredIqs[(fro, ID)]
                        return
 
-               if(iqType not in ["get", "set"]): return # Not interested       
+               if not (iqType == "get" or iqType == "set"): return # Not interested    
 
-               debug.log("Discovery: Iq received \"%s\" \"%s\". Looking for handler" % (fro, ID))
+               LogEvent(INFO, "", "Looking for handler")
 
                for query in el.elements():
                        xmlns = query.defaultUri
+                       node = query.getAttribute("node")
                        
-                       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)
+                       if xmlns.startswith(DISCO) and node:
+                               if self.nodes.has_key(to) and self.nodes[to].has_key(node) and self.nodes[to][node][0]:
+                                       self.nodes[to][node][0](el)
+                                       return
                                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)
+                                       # If the node we're browsing wasn't found, fall through and display the root disco
+                                       self.sendDiscoInfoResponse(to=fro, ID=ID, ulang=ulang, jid=to)
+                                       return
+                       elif xmlns == DISCO_INFO:
+                               self.sendDiscoInfoResponse(to=fro, ID=ID, ulang=ulang, jid=to)
+                               return
+                       elif xmlns == DISCO_ITEMS:
+                               self.sendDiscoItemsResponse(to=fro, ID=ID, ulang=ulang, jid=to)
+                               return
+
+                       if to.find('@') > 0:
+                               searchjid = "USER"
+                       else:
+                               searchjid = to
+                       for (feature, handler) in self.features.get(searchjid, []):
+                               if feature == xmlns and handler:
+                                       LogEvent(INFO, "Handler found")
+                                       handler(el)
+                                       return
+
+                       # Still hasn't been handled
+                       LogEvent(WARN, "", "Unknown Iq request")
+                       self.sendIqError(to=fro, fro=to, ID=ID, xmlns=DISCO, etype="cancel", condition="feature-not-implemented")
        
-       def sendDiscoInfoResponse(self, to, ID):
-               debug.log("Discovery: Replying to disco#info request from \"%s\" \"%s\"" % (to, ID))
+       def sendDiscoInfoResponse(self, to, ID, ulang, jid):
+               """ Send a service discovery disco#info stanza to the given 'to'. 'jid' is the JID that was queried. """
+               LogEvent(INFO)
                iq = Element((None, "iq"))
                iq.attributes["type"] = "result"
-               iq.attributes["from"] = config.jid
+               iq.attributes["from"] = jid
                iq.attributes["to"] = to
                if(ID):
                        iq.attributes["id"] = ID
                query = iq.addElement("query")
                query.attributes["xmlns"] = DISCO_INFO
                
+               searchjid = jid
+               if(jid.find('@') > 0): searchjid = "USER"
                # Add any identities
-               for (category, ctype, name) in self.identities:
+               for (category, ctype, name) in self.identities.get(searchjid, []):
                        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:
+               for (var, handler) in self.features.get(searchjid, []):
                        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))
+       def sendDiscoItemsResponse(self, to, ID, ulang, jid):
+               """ Send a service discovery disco#items stanza to the given 'to'. 'jid' is the JID that was queried. """
+               LogEvent(INFO)
                iq = Element((None, "iq"))
                iq.attributes["type"] = "result"
-               iq.attributes["from"] = config.jid
+               iq.attributes["from"] = jid
                iq.attributes["to"] = to
                if(ID):
                        iq.attributes["id"] = ID
                query = iq.addElement("query")
                query.attributes["xmlns"] = DISCO_ITEMS
+
+               searchjid = jid
+               if(jid.find('@') > 0): searchjid = "USER"
+               for node in self.nodes.get(searchjid, []):
+                       handler, name, rootnode = self.nodes[jid][node]
+                       if rootnode:
+                               name = getattr(lang.get(ulang), name)
+                               item = query.addElement("item")
+                               item.attributes["jid"] = jid
+                               item.attributes["node"] = node
+                               item.attributes["name"] = name
                
                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
+       def sendIqError(self, to, fro, ID, xmlns, etype, condition):
+               """ Sends an IQ error response. See the XMPP RFC for details on the fields. """
+               el = Element((None, "iq"))
+               el.attributes["to"] = to
+               el.attributes["from"] = fro
                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)
+                       el.attributes["id"] = ID
+               el.attributes["type"] = "error"
+               error = el.addElement("error")
+               error.attributes["type"] = etype
+               error.attributes["code"] = str(utils.errorCodeMap[condition])
+               cond = error.addElement(condition)
+               self.pytrans.send(el)
+
 
index 2eeef61a5b22fdff0ac3ca18f9c60cdb31371ce4..27b4d0ef1ce29afaaf09f2fdae2d2ea1567c6e0f 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Copyright 2004-2005 James Bunton <james@delx.cjb.net>
 # Licensed for distribution under the GPL version 2, check COPYING for details
 
 import utils
@@ -7,9 +7,10 @@ if(utils.checkTwisted()):
        from twisted.xish.domish import Element
 else:
        from tlib.domish import Element
+from debug import LogEvent, INFO, WARN, ERROR
+import disco
 import jabw
 import config
-import debug
 import lang
 import string
 import time
@@ -33,7 +34,7 @@ class BaseGroupchat:
                
                self.checkTimer = reactor.callLater(60.0*2, self.checkUserJoined, None)
                
-               debug.log("BaseGroupchat: \"%s\" created" % (self.roomJID()))
+               LogEvent(INFO, self.roomJID())
        
        def removeMe(self):
                """ Cleanly removes the object """
@@ -47,9 +48,9 @@ class BaseGroupchat:
                        self.checkTimer.cancel()
                self.checkTimer = None
 
+               LogEvent(INFO, self.roomJID())
+
                utils.mutilateMe(self)
-               
-               debug.log("BaseGroupchat: \"%s\" destroyed" % (self.roomJID()))
        
        def roomJID(self):
                """ Returns the room JID """
@@ -66,7 +67,7 @@ class BaseGroupchat:
        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.")
+                       LogEvent(INFO, self.roomJID(), "User hasn't joined after two minutes. Removing them from the room.")
                        
                        text = []
                        text.append(lang.get(self.session.lang).groupchatFailJoin1 % (self.roomJID()))
@@ -89,6 +90,7 @@ class BaseGroupchat:
        
        def sendUserInvite(self, fro):
                """ Sends the invitation out to the Jabber user to join this room """
+               LogEvent(INFO, self.roomJID(), "Sending invitation to user")
                el = Element((None, "message"))
                el.attributes["from"] = fro
                el.attributes["to"] = self.user()
@@ -97,8 +99,7 @@ class BaseGroupchat:
                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()))
+               x.attributes["xmlns"] = disco.XCONFERENCE
                self.session.pytrans.send(el)
        
        def userJoined(self, nick):
@@ -108,7 +109,7 @@ class BaseGroupchat:
                        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()))
+                       LogEvent(INFO, self.roomJID())
                        self.ready = True
                        for (source, text, timestamp) in self.messageBuffer:
                                self.messageReceived(source, text, timestamp)
@@ -119,14 +120,14 @@ class BaseGroupchat:
        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))
+                       LogEvent(INFO, self.roomJID())
                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))
+                       LogEvent(INFO, self.roomJID())
                self.contactPresenceChanged(contact, ptype="unavailable")
                self.messageReceived(None, "%s has left the conference." % (contact))
        
@@ -135,10 +136,11 @@ class BaseGroupchat:
                        timestamp = time.strftime("%Y%m%dT%H:%M:%S")
                        self.messageBuffer.append((source, message, timestamp))
                else:
+                       self.session.pytrans.statistics.stats["MessageCount"] += 1
                        fro = self.roomJID()
                        if(source):
                                fro += "/" + source
-                       debug.log("BaseGroupchat: \"%s\" messageReceived(\"%s\", \"%s\", \"%s\")" % (self.roomJID(), source, message, timestamp))
+                       LogEvent(INFO, self.roomJID())
                        self.session.sendMessage(to=self.user(), fro=fro, body=message, mtype="groupchat", delay=timestamp)
        
        def contactPresenceChanged(self, contact, ptype=None):
@@ -147,7 +149,7 @@ class BaseGroupchat:
                        self.session.sendPresence(to=self.user(), fro=fro, ptype=ptype)
        
        def sendMessage(self, text, noerror):
-               debug.log("BaseGroupchat: \"%s\" sendMessage(\"%s\")" % (self.roomJID(), text))
+               LogEvent(INFO, self.roomJID())
                self.messageReceived(self.nick, text)
                self.sendLegacyMessage(text, noerror)
        
@@ -158,3 +160,5 @@ class BaseGroupchat:
        def sendContactInvite(self, contact):
                """ Reimplement this to send the packet to the legacy service """
                pass
+
+
diff --git a/src/housekeep.py b/src/housekeep.py
new file mode 100644 (file)
index 0000000..6a472ca
--- /dev/null
@@ -0,0 +1,117 @@
+# Copyright 2005 James Bunton <james@delx.cjb.net>
+# Licensed for distribution under the GPL version 2, check COPYING for details
+
+import utils
+import config
+import xdb
+if(utils.checkTwisted()):
+       from twisted.words.protocols.jabber import jid
+else:
+       from tlib.jabber import jid
+
+import shutil
+import os
+import os.path
+
+
+def init():
+       global noteList
+       global noteListF
+
+       try:
+               notes = NotesToMyself()
+               for note in noteList:
+                       if notes.check(note):
+                               noteListF[noteList.index(note)]()
+                               notes.append(note)
+               notes.save()
+       except:
+               print "An error occurred during one of the automatic data update routines. Please report this bug."
+               raise
+
+
+class NotesToMyself:
+       def __init__(self):
+               pre = os.path.abspath(config.spooldir) + "/" + config.jid + "/"
+               self.filename = pre + "/notes_to_myself"
+               self.notes = []
+               
+               if os.path.exists(self.filename):
+                       f = open(self.filename, "r")
+                       self.notes = [x.strip() for x in f.readlines()]
+                       f.close()
+               elif not os.path.exists(pre):
+                       global noteList
+                       self.notes = noteList
+                       os.makedirs(pre)
+       
+       def check(self, note):
+               return self.notes.count(note) == 0
+       
+       def append(self, note):
+               if self.check(note):
+                       self.notes.append(note)
+
+       def save(self):
+               f = open(self.filename, "w")
+               for note in self.notes:
+                       f.write(note + "\n")
+               f.close()
+
+
+
+def doSpoolPrepCheck():
+       pre = os.path.abspath(config.spooldir) + "/" + config.jid + "/"
+
+       print "Checking spool files and stringprepping any if necessary...",
+
+       for file in os.listdir(pre):
+               file = xdb.unmangle(file).decode("utf-8")
+               filej = jid.JID(file).full()
+               if(file != filej):
+                       file = xdb.mangle(file)
+                       filej = xdb.mangle(filej)
+                       if(os.path.exists(filej)):
+                               print "Need to move", file, "to", filej, "but the latter exists!\nAborting!"
+                               os.exit(1)
+                       else:
+                               shutil.move(pre + file, pre + filej)
+       print "done"
+
+
+def doHashDirUpgrade():
+       print "Upgrading your XDB structure to use hashed directories for speed...",
+
+       # Do avatars...
+       pre = os.path.abspath(config.spooldir) + "/" + config.jid + "/avatars/"
+       if os.path.exists(pre):
+               for file in os.listdir(pre):
+                       if os.path.isfile(pre + file):
+                               pre2 = pre + file[0:3] + "/"
+                               if not os.path.exists(pre2):
+                                       os.makedirs(pre2)
+                               shutil.move(pre + file, pre2 + file)
+       
+       # Do spool files...
+       pre = os.path.abspath(config.spooldir) + "/" + config.jid + "/"
+       if os.path.exists(pre):
+               for file in os.listdir(pre):
+                       if os.path.isfile(pre + file) and file != "notes_to_myself":
+                               hash = file[0:2]
+                               pre2 = pre + hash + "/"
+                               if not os.path.exists(pre2):
+                                       os.makedirs(pre2)
+
+                               if(os.path.exists(pre2 + file)):
+                                       print "Need to move", file, "to", pre2 + file, "but the latter exists!\nAborting!"
+                                       os.exit(1)
+                               else:
+                                       shutil.move(pre + file, pre2 + file)
+
+       print "done"
+
+
+
+noteList = ["doSpoolPrepCheck", "doHashDirUpgrade"]
+noteListF = [doSpoolPrepCheck, doHashDirUpgrade]
+
index fa7f4f44cda807a45b795cdc2fece7fc027733ff..0d0a1f51604e886ed0fee0b75bf1290b0b7aacea 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Copyright 2004-2005 James Bunton <james@delx.cjb.net>
 # Licensed for distribution under the GPL version 2, check COPYING for details
 
 import utils
@@ -8,12 +8,13 @@ if(utils.checkTwisted()):
 else:
        from tlib.domish import Element
        from tlib.jabber import jid
-import debug
+from debug import LogEvent, INFO, WARN, ERROR
+import disco
 
 
 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))
+       LogEvent(INFO)
        el = Element((None, "message"))
        el.attributes["to"] = to
        el.attributes["from"] = fro
@@ -23,18 +24,18 @@ def sendMessage(pytrans, to, fro, body, mtype=None, delay=None):
        
        if(delay):
                x = el.addElement("x")
-               x.attributes["xmlns"] = "jabber:x:delay"
+               x.attributes["xmlns"] = disco.XDELAY
                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"
+       x.attributes["xmlns"] = disco.XEVENT
        composing = x.addElement("composing")
        pytrans.send(el)
 
-def sendPresence(pytrans, to, fro, show=None, status=None, priority=None, ptype=None):
+def sendPresence(pytrans, to, fro, show=None, status=None, priority=None, ptype=None, avatarHash=None, nickname=None, payload=[]):
        # Strip the resource off any presence subscribes (as per XMPP RFC 3921 Section 5.1.6)
        # Makes eJabberd behave :)
        if(ptype == "subscribe"):
@@ -55,6 +56,25 @@ def sendPresence(pytrans, to, fro, show=None, status=None, priority=None, ptype=
        if(priority):
                s = el.addElement("priority")
                s.addContent(priority)
+
+       if(not ptype):
+               x = el.addElement("x")
+               x.attributes["xmlns"] = disco.XVCARDUPDATE
+               if(avatarHash):
+                       xx = el.addElement("x")
+                       xx.attributes["xmlns"] = disco.XAVATAR
+                       h = xx.addElement("hash")
+                       h.addContent(avatarHash)
+                       h = x.addElement("photo")
+                       h.addContent(avatarHash)
+               if(nickname):
+                       n = x.addElement("nickname")
+                       n.addContent(nickname)
+       
+       if(payload):
+               for p in payload:
+                       el.addChild(p)
+
        pytrans.send(el)
 
 
@@ -91,35 +111,28 @@ class JabberConnection:
                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))
+               LogEvent(INFO, 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
+               LogEvent(INFO, self.jabberID)
        
        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))
+               LogEvent(INFO, self.jabberID)
                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))
+                       LogEvent(INFO, self.jabberID)
                        el = Element((None, "message"))
                        el.attributes["to"] = to
                        el.attributes["from"] = fro
                        x = el.addElement("x")
-                       x.attributes["xmlns"] = "jabber:x:event"
+                       x.attributes["xmlns"] = disco.XEVENT
                        if(typing):
                                composing = x.addElement("composing") 
                        id = x.addElement("id")
@@ -127,14 +140,28 @@ class JabberConnection:
                                id.addContent(self.messageIDs[fro])
                        self.pytrans.send(el)
        
+       def sendVCardRequest(self, to, fro):
+               """ Requests the the vCard of 'to'
+               Returns a Deferred which fires when the vCard has been received.
+               First argument an Element object of the vCard
+               """
+               el = Element((None, "iq"))
+               el.attributes["to"] = to
+               el.attributes["from"] = fro
+               el.attributes["type"] = "get"
+               el.attributes["id"] = self.pytrans.makeMessageID()
+               vCard = el.addElement("vCard")
+               vCard.attributes["xmlns"] = "vcard-temp"
+               return self.pytrans.discovery.sendIq(el)
+       
        def sendErrorMessage(self, to, fro, etype, condition, explanation, body=None):
-               debug.log("User: %s - JabberConnection sending error response." % (self.jabberID))
+               LogEvent(INFO, self.jabberID)
                sendErrorMessage(self.pytrans, to, fro, etype, condition, explanation, body)
        
-       def sendPresence(self, to, fro, show=None, status=None, priority=None, ptype=None):
+       def sendPresence(self, to, fro, show=None, status=None, priority=None, ptype=None, avatarHash=None, nickname=None, payload=[]):
                """ 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)
+               LogEvent(INFO, self.jabberID)
+               sendPresence(self.pytrans, to, fro, show, status, priority, ptype, avatarHash, nickname, payload)
        
        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
@@ -144,7 +171,7 @@ class JabberConnection:
                el.attributes["from"] = jid
                el.attributes["type"] = ptype
                r = el.addElement("x")
-               r.attributes["xmlns"] = "http://jabber.org/protocol/roster-subsync"
+               r.attributes["xmlns"] = disco.SUBSYNC
                item = r.addElement("item")
                item.attributes["subscription"] = sub
                if(name):
@@ -157,38 +184,50 @@ class JabberConnection:
        
        def onMessage(self, el):
                """ Handles incoming message packets """
-               if(not self.checkFrom(el)): return
-               debug.log("User: %s - JabberConnection received message packet" % (self.jabberID))
+               #LogEvent(INFO, self.jabberID)
                fro = el.getAttribute("from")
-               froj = jid.JID(fro)
                to = el.getAttribute("to")
-               toj = jid.JID(to)
+               try:
+                       froj = jid.JID(fro)
+                       toj = jid.JID(to)
+               except Exception, e:
+                       LogEvent(WARN, self.jabberID)
+                       return
+
                mID = el.getAttribute("id")
-               
                mtype = el.getAttribute("type")
                body = ""
-               invite = ""
+               inviteTo = ""
+               inviteRoom = ""
                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"):
+                       elif(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"):
+                       elif(child.name == "x"):
+                               if(child.uri == disco.XCONFERENCE):
+                                       inviteTo = to
+                                       inviteRoom = child.getAttribute("jid") # The room the contact is being invited to
+                               elif(child.uri == disco.MUC_USER):
+                                       for child2 in child.elements():
+                                               if(child2.name == "invite"):
+                                                       inviteTo = child2.getAttribute("to")
+                                                       break
+                                       inviteRoom = to
+                               elif(child.uri == disco.XEVENT):
                                        messageEvent = True
                                        composing = False
-                                       for deepchild in child.elements():
-                                               if(deepchild.name == "composing"):
+                                       for child2 in child.elements():
+                                               if(child2.name == "composing"):
                                                        composing = True
+                                                       break
                
-               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)
+               if(inviteTo and inviteRoom):
+                       LogEvent(INFO, self.jabberID, "Message groupchat invite packet")
+                       self.inviteReceived(source=froj.userhost(), resource=froj.resource, dest=inviteTo, destr="", roomjid=inviteRoom)
                        return
 
                # Check message event stuff
@@ -197,21 +236,19 @@ class JabberConnection:
                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))
+                       LogEvent(INFO, self.jabberID, "Message typing notification packet")
                        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)))
+                       LogEvent(INFO, self.jabberID, "Message packet")
                        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))
+               #LogEvent(INFO, self.jabberID)
                fro = el.getAttribute("from")
                froj = jid.JID(fro)
                to = el.getAttribute("to")
@@ -219,13 +256,15 @@ class JabberConnection:
                
                # 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))
+               if ptype and (ptype.startswith("subscribe") or ptype.startswith("unsubscribe")):
+                       LogEvent(INFO, self.jabberID, "Parsed subscription presence packet")
                        self.subscriptionReceived(toj.userhost(), ptype)
                else:
                        status = None
                        show = None
                        priority = None
+                       avatarHash = ""
+                       nickname = ""
                        for child in el.elements():
                                if(child.name == "status"):
                                        status = child.__str__()
@@ -233,8 +272,22 @@ class JabberConnection:
                                        show = child.__str__()
                                elif(child.name == "priority"):
                                        priority = child.__str__()
+                               elif(child.defaultUri == disco.XVCARDUPDATE):
+                                       avatarHash = " "
+                                       for child2 in child.elements():
+                                               if(child2.name == "photo"):
+                                                       avatarHash = child2.__str__()
+                                               elif(child2.name == "nickname"):
+                                                       nickname = child2.__str__()
+
+                       if not ptype:
+                               # available presence
+                               if(avatarHash):
+                                       self.avatarHashReceived(froj.userhost(), toj.userhost(), avatarHash)
+                               if(nickname):
+                                       self.nicknameReceived(froj.userhost(), toj.userhost(), nickname)
                        
-                       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)))
+                       LogEvent(INFO, self.jabberID, "Parsed presence packet")
                        self.presenceReceived(froj.userhost(), froj.resource, toj.userhost(), toj.resource, priority, ptype, show, status)
        
        
@@ -254,6 +307,13 @@ class JabberConnection:
        def subscriptionReceived(self, source, subtype):
                """ Override this method to be notified when a subscription packet is received """
                pass
-
+       
+       def nicknameReceived(self, source, dest, nickname):
+               """ Override this method to be notified when a nickname has been received """
+               pass
+       
+       def avatarHashReceieved(self, source, dest, avatarHash):
+               """ Override this method to be notified when an avatar hash is received """
+               pass
 
 
index ecc73277472b616533b0f94f9f40b0ee33bc609d..0e616e44ad231023fe8314897841e7ff242677d1 100644 (file)
@@ -3,7 +3,7 @@
 import config
 
 def get(lang=config.lang):
-       if(not lang.__class__ in [str, unicode]):
+       if not (lang.__class__ == str or lang.__class__ == unicode):
                lang = config.lang
        try:
                lang = lang.replace("-", "_")
@@ -20,7 +20,7 @@ def get(lang=config.lang):
 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"
+               registerText = u"Please type your MSN Passport (user@hotmail.com) into the username field and your password.\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."
@@ -36,9 +36,29 @@ class strings:
                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"
+               msnDroppedMessage = u"(Automated message)\nA message from this person did not get delivered to you. Please report this to your Jabber server administrator."
                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"
+
+               command_CommandList = u"PyMSNt Commands"
+               command_Done = "Command completed."
+               command_ConnectUsers = u"Connect all registered users"
+               command_Statistics = u"Statistics for PyMSNt"
+               command_OnlineUsers = u"Online Users"
+               command_TotalUsers = u"Total Users"
+               command_Uptime = u"Uptime"
+               command_MessageCount = u"Message Count"
+               command_FailedMessageCount = u"Failed Message Count"
+               command_AvatarCount = u"Avatar Count"
+               command_FailedAvatarCount = u"Failed Avatar Count"
+               command_OnlineUsers_Desc = u"The number of users currently connected to the service."
+               command_TotalUsers_Desc = u"The number of connections since the service started."
+               command_Uptime_Desc = u"How long the service has been running, in seconds."
+               command_MessageCount_Desc = u"How many messages have been transferred to and from the MSN network."
+               command_FailedMessageCount_Desc = u"The number of messages that didn't make it to the MSN recipient and were bounced."
+               command_AvatarCount_Desc = u"How many avatars have been transferred to and from the MSN network."
+               command_FailedAvatarCount_Desc = u"The number of avatar transfers that have failed."
        en_US = en # en-US is the same as en, so are the others
        en_AU = en
        en_GB = en
@@ -61,30 +81,70 @@ class strings:
                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"
+               msnDroppedMessage = u"(Automated message)\nA message from this person did not get delivered to you. Please report this to your Jabber server administrator."
                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>
+               command_CommandList = u"PyMSNt Commands"
+               command_Done = "Command completed."
+               command_ConnectUsers = u"Connect all registered users"
+               command_Statistics = u"Statistics for PyMSNt"
+               command_OnlineUsers = u"Online Users"
+               command_TotalUsers = u"Total Users"
+               command_Uptime = u"Uptime"
+               command_MessageCount = u"Message Count"
+               command_AvatarCount = u"Avatar Count"
+               command_FailedAvatarCount = u"Failed Avatar Count"
+               command_FailedMessageCount = u"Failed Message Count"
+               command_OnlineUsers_Desc = u"The number of users currently connected to the service."
+               command_TotalUsers_Desc = u"The number of connections since the service started."
+               command_Uptime_Desc = u"How long the service has been running, in seconds."
+               command_MessageCount_Desc = u"How many messages have been transferred to and from the MSN network."
+               command_FailedMessageCount_Desc = u"The number of messages that didn't make it to the MSN recipient and were bounced."
+               command_AvatarCount_Desc = u"How many avatars have been transferred to and from the MSN network."
+               command_FailedAvatarCount_Desc = u"The number of avatar transfers that have failed."
+
+       class nl: # Dutch - Matthias Therry <matthias.therry@pi.be>, Sander Devrieze <s.devrieze@pandora.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."
+               notRegistered = u"Fout: 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"Fout: 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."
+               groupchatPrivateError = u"Fout: 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 het 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"
+               msnDroppedMessage = u"(Automatisch bericht)\nEen bericht van deze persoon raakte niet to bij jou. Breng de beheerder van uw Jabber-server hiervan op de hoogte."
                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"
+
+               command_CommandList = u"Commando's voor PyMSNt"
+               command_Done = "Commando beëindigd."
+               command_ConnectUsers = u"Alle geregistreerde gebruikers verbinden"
+               command_Statistics = u"Statistieken van PyMSNt"
+               command_OnlineUsers = u"Online gebruikers"
+               command_TotalUsers = u"Totaal aantal gebruikers"
+               command_Uptime = u"Uptime"
+               command_MessageCount = u"Aantal berichten"
+               command_AvatarCount = u"Aantal avatars"
+               command_FailedAvatarCount = u"Telling van avatars mislukt"
+               command_FailedMessageCount = u"Telling van berichten mislukt"
+               command_OnlineUsers_Desc = u"Het aantal gebruikers die momenteel dit transport gebruiken."
+               command_TotalUsers_Desc = u"Het aantal verbindingen sinds het transport gestart werd."
+               command_Uptime_Desc = u"Hoelang het transport al draait (seconden)."
+               command_MessageCount_Desc = u"Hoeveel berichten er van en naar het MSN-netwerk overgebracht werden."
+               command_FailedMessageCount_Desc = u"Het aantal berichten die zijn ontvanger op het MSN-netwerk niet bereikten en dus teruggestuurd werden."
+               command_AvatarCount_Desc = u"Hoeveel avatars er van en naar het MSN-netwerk overgebracht werden."
+               command_FailedAvatarCount_Desc = u"Het aantal overdrachten van avatars die mislukt zijn."
        dut = nl
        nla = nl
        
@@ -106,11 +166,31 @@ class strings:
                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"
+               msnFailedMessage = 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"
+               msnDroppedMessage = u"(Automated message)\nA message from this person did not get delivered to you. Please report this to your Jabber server administrator."
                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"
 
+               command_CommandList = u"PyMSNt Commands"
+               command_Done = "Command completed."
+               command_ConnectUsers = u"Connect all registered users"
+               command_Statistics = u"Statistics for PyMSNt"
+               command_OnlineUsers = u"Online Users"
+               command_TotalUsers = u"Total Users"
+               command_Uptime = u"Uptime"
+               command_MessageCount = u"Message Count"
+               command_AvatarCount = u"Avatar Count"
+               command_FailedAvatarCount = u"Failed Avatar Count"
+               command_FailedMessageCount = u"Failed Message Count"
+               command_OnlineUsers_Desc = u"The number of users currently connected to the service."
+               command_TotalUsers_Desc = u"The number of connections since the service started."
+               command_Uptime_Desc = u"How long the service has been running, in seconds."
+               command_MessageCount_Desc = u"How many messages have been transferred to and from the MSN network."
+               command_FailedMessageCount_Desc = u"The number of messages that didn't make it to the MSN recipient and were bounced."
+               command_AvatarCount_Desc = u"How many avatars have been transferred to and from the MSN network."
+               command_FailedAvatarCount_Desc = u"The number of avatar transfers that have failed."
+
 
        class fr: # French - Lucas Nussbaum <lucas@lucas-nussbaum.net>
                # Former translator: Alexandre Viard <mailto:ebola@courrier.homelinux.org>
@@ -133,6 +213,25 @@ class strings:
                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"
+               
+               command_CommandList = u"Commandes PyMSNt"
+               command_Done = "Command completed."
+               command_ConnectUsers = u"Connect all registered users"
+               command_Statistics = u"Statistiques de PyMSNt"
+               command_OnlineUsers = u"Utilisateurs connectés"
+               command_TotalUsers = u"Nombre total de connexions depuis le démarrage du service"
+               command_Uptime = u"Uptime"
+               command_MessageCount = u"Nombre de messages"
+               command_AvatarCount = u"Nombre d'avatars"
+               command_FailedAvatarCount = u"Nombre d'avatars avec Ã©chec"
+               command_FailedMessageCount = u"Nombre de messages avec Ã©chec"
+               command_OnlineUsers_Desc = u"Nombre d'utilisateurs connectés Ã  ce service actuellement"
+               command_TotalUsers_Desc = u"Nombre total de connexions depuis le démarrage du service"
+               command_Uptime_Desc = u"Durée de fonctionnement du service (en secondes)"
+               command_MessageCount_Desc = u"Nombre de messages transférés depuis et vers le réseau MSN"
+               command_FailedMessageCount_Desc = u"Nombre de messages qui n'ont pas pu Ãªtre transférés vers le réseau MSN"
+               command_AvatarCount_Desc = u"Nombre d'avatars transférés depuis et vers le réseau MSN"
+               command_FailedAvatarCount_Desc = u"Nombre d'avatars qui n'ont pas pu Ãªtre transférés"
        fr_FR = fr
        fr_LU = fr
        fr_CH = fr
@@ -157,9 +256,29 @@ class strings:
                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"
+               msnDroppedMessage = u"(Automated message)\nA message from this person did not get delivered to you. Please report this to your Jabber server administrator."
                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"
+
+               command_CommandList = u"PyMSNt Commands"
+               command_Done = "Command completed."
+               command_ConnectUsers = u"Connect all registered users"
+               command_Statistics = u"Statistics for PyMSNt"
+               command_OnlineUsers = u"Online Users"
+               command_TotalUsers = u"Total Users"
+               command_Uptime = u"Uptime"
+               command_MessageCount = u"Message Count"
+               command_AvatarCount = u"Avatar Count"
+               command_FailedAvatarCount = u"Failed Avatar Count"
+               command_FailedMessageCount = u"Failed Message Count"
+               command_OnlineUsers_Desc = u"The number of users currently connected to the service."
+               command_TotalUsers_Desc = u"The number of connections since the service started."
+               command_Uptime_Desc = u"How long the service has been running, in seconds."
+               command_MessageCount_Desc = u"How many messages have been transferred to and from the MSN network."
+               command_FailedMessageCount_Desc = u"The number of messages that didn't make it to the MSN recipient and were bounced."
+               command_AvatarCount_Desc = u"How many avatars have been transferred to and from the MSN network."
+               command_FailedAvatarCount_Desc = u"The number of avatar transfers that have failed."
        es_ES = es
        es_AR = es
        es_BO = es
@@ -180,4 +299,45 @@ class strings:
        es_DO = es
        es_CR = es
        
+       class pl: # Polish - Tomasz Sterna <xmpp:smoku@chrome.pl>
+               registerText = u"Wpisz proszÄ™ swój Paszport MSN (użytkownik@hotmail.com) w pola użytkownik i hasÅ‚o."
+               gatewayTranslator = u"Wpisz konto użytkownika MSN."
+               userMapping = u"Kontakt MSN %s ma Jabber ID %s. Zaleca siÄ™ rozmawianie z tÄ… osobÄ… przez Jabbera."
+               notLoggedIn = u"BÅ‚Ä…d. Musisz zalogować siÄ™ do transportu zanim zaczniesz wysyÅ‚ać wiadomoÅ›ci."
+               notRegistered = u"Przykro mi. WyglÄ…da na to, Å¼e nie zarejestrowaÅ‚eÅ› siÄ™ jeszcze w tym transporcie. Zarejestruj siÄ™ i spróbuj ponownie."
+               waitForLogin = u"Wybacz, ale nie można jeszcze dostarczyć tej wiadomoÅ›ci. Spróbuj ponownie gdy transport zakoÅ„czy logowanie siÄ™."
+               groupchatInvite = u"OtrzymaÅ‚eÅ› zaproszenie do rozmowy grupowej na obcej usÅ‚udze. Musisz wejść do pokoju rozmów %s aby doÅ‚Ä…czyć do tej rozmowy.\nJeÅ›li nie wejdziesz do tego pokoju, nie bÄ™dziesz mógÅ‚ uczestniczyć w rozmowie grupowej, ale kontaktom MSN bÄ™dzie siÄ™ wydawaÅ‚o, Å¼e uczestniczysz."
+               groupchatFailJoin1 = u"Nie doÅ‚Ä…czyÅ‚eÅ› do pokoju rozmów %s.\nByli w nim nastÄ™pujÄ…cy użytkownicy:"
+               groupchatFailJoin2 = u"ZostaÅ‚eÅ› usuniÄ™ty z tego pokoju rozmów na obcej usÅ‚udze. W czasie gdy wyglÄ…daÅ‚o, Å¼e uczestniczysz w rozmowie wyglÄ…daÅ‚a ona tak."
+               groupchatPrivateError = u"Wybacz, ale nie możesz wysyÅ‚ać prywatnych wiadomoÅ›ci do uczestników tej rozmowy. Dodaj użytkownika do swojej listy kontaktów i napisz do niego używajÄ…c jej."
+               groupchatAdvocacy = u"%s zaprosiÅ‚ ciÄ™ na Jabberowego czata. Aby do niego doÅ‚Ä…czyć musisz używać Jabbera. WiÄ™cej informacji znajdziesz na %s."
+               msnMaintenance = u"Wiadomość od Microsoftu. Sieć MSN zostanie chwilowo wyÅ‚Ä…czona z powodu prac serwisowych."
+               msnMultipleLogin = u"Twoje konto MSN zalogowaÅ‚o siÄ™ gdzieÅ› indziej. Wyloguj proszÄ™ tÄ™ lokacjÄ™ i reaktywuj transport MSN."
+               msnNotVerified = u"Adres email Twojego Paszportu MSN %s, nie zostaÅ‚ potwierdzony. Użytkownicy MSN nie bÄ™dÄ… widzieli Twojego nicka, oraz bÄ™dÄ… ostrzegani, Å¼e Twoje konto może nie być wiarygodne. WiÄ™cej informacji znajdziesz u Microsoftu."
+               msnLoginFailure = u"Transport MSN nie mógÅ‚ zalogować siÄ™ na konto MSN %s. Sprawdź proszÄ™, czy hasÅ‚o jest wÅ‚aÅ›ciwe. Może być konieczna ponowna rejestracja w transporcie."
+               msnFailedMessage = u"Wiadomość nie mogÅ‚a zostać dostarczona. Sprawdź proszÄ™ czy kontakt jest online i czy jego adres na Twojej liÅ›cie kontaktów jest dobry.\n\n"
+               msnDroppedMessage = u"(Wiadomość automatyczna)\nWiadomość od tej osoby nie mogÅ‚a zostać dostarczona do Ciebie. ZgÅ‚oÅ› to proszÄ™ swojemu administratorowi serwera Jabbera."
+               msnInitialMail = u"Powiadomienie Hotmail\n\nNieprzeczytane wiadomoÅ›ci w skrzynce odbiorczej: %s\nNieprzeczytane wiadomoÅ›ci w folderach: %s"
+               msnRealtimeMail = u"Powiadomienie Hotmail\n\nOd: %s <%s>\n Temat: %s"
+               msnDisconnected = u"RozÅ‚Ä…czenie z sieci MSN: %s"
+
+               command_CommandList = u"Polecenia PyMSNt"
+               command_Done = "Polecenie zakoÅ„czone."
+               command_ConnectUsers = u"PodÅ‚Ä…cz wszystkich zarejestrowanych użytkowników"
+               command_Statistics = u"Statystyki PyMSNt"
+               command_OnlineUsers = u"Użytkownicy Online"
+               command_TotalUsers = u"Użytkownicy ogółem"
+               command_Uptime = u"Uptime"
+               command_MessageCount = u"Licznik wiadomoÅ›ci"
+               command_FailedMessageCount = u"Licznik nieudanych wiadomoÅ›ci"
+               command_AvatarCount = u"Licznik Awatarów"
+               command_FailedAvatarCount = u"Licznik nieudanych Awatarów"
+               command_OnlineUsers_Desc = u"Użytkownicy aktualnie podÅ‚Ä…czeni do usÅ‚ugi."
+               command_TotalUsers_Desc = u"Liczba poÅ‚Ä…czeÅ„ od uruchomienia usÅ‚ugi."
+               command_Uptime_Desc = u"Jak dÅ‚ugo dziaÅ‚a usÅ‚uga, w sekundach."
+               command_MessageCount_Desc = u"Ile wiadomoÅ›ci przesÅ‚ano do i z sieci MSN."
+               command_FailedMessageCount_Desc = u"Liczba wiadomoÅ›ci które nie dotarÅ‚y do użytkowników MSN i zostaÅ‚y odbite."
+               command_AvatarCount_Desc = u"Ile awatarów zostaÅ‚o przesÅ‚anych z i do sieci MSN."
+               command_FailedAvatarCount_Desc = u"Liczba nieudanych przesyłów awatara."
+       pl_PL = pl
 
index 0df7da3c8dc4e058ad3ab4c9791dda350b2bd933..6e5a50afc672206ea5a35f10c316f170a89308f1 100644 (file)
@@ -1,6 +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 LegacyConnection, LegacyGroupchat, translateAccount, startStats, updateStats
+from glue import name, url, version, mangle, id, namespace
 from glue import formRegEntry, getAttributes, isGroupJID
diff --git a/src/legacy/defaultAvatar.png b/src/legacy/defaultAvatar.png
new file mode 100644 (file)
index 0000000..5d37a21
Binary files /dev/null and b/src/legacy/defaultAvatar.png differ
diff --git a/src/legacy/defaultJabberAvatar.png b/src/legacy/defaultJabberAvatar.png
new file mode 100644 (file)
index 0000000..0427e1f
Binary files /dev/null and b/src/legacy/defaultJabberAvatar.png differ
index 17474924cabe93dd0981c76bfa6192498997a535..a809f8eed0b6eb2e2f275d6deb34398ef8f39f6c 100644 (file)
@@ -1,28 +1,36 @@
-# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Copyright 2004-2005 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()):
+if utils.checkTwisted():
        from twisted.xish.domish import Element
 else:
        from tlib.domish import Element
-from tlib import msn
+from tlib import msn, msnp2p
+from debug import LogEvent, INFO, WARN, ERROR
+import sha
 import groupchat
+import avatar
 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
+name = "MSN Transport"   # The name of the transport
+url = "http://msn-transport.jabberstudio.org"
+version = "0.10.1"       # The transport version
+mangle = True            # XDB '@' -> '%' mangling
+id = "msn"               # The transport identifier
 
 
+# Load the default avatar
+f = open("src/legacy/defaultJabberAvatar.png")
+defaultJabberAvatarData = f.read()
+f.close()
+
 
 def isGroupJID(jid):
        """ Returns True if the JID passed is a valid groupchat JID (for MSN, does not contain '%') """
@@ -34,7 +42,7 @@ def isGroupJID(jid):
 namespace = "jabber:iq:register"
 
 
-def formRegEntry(username, password, nickname):
+def formRegEntry(username, password):
        """ 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"
@@ -45,9 +53,6 @@ def formRegEntry(username, password, nickname):
        passEl = reginfo.addElement("password")
        passEl.addContent(password)
 
-       nickEl = reginfo.addElement("nick")
-       if(nickname): nickEl.addContent(nickname)
-       
        return reginfo
 
 
@@ -55,25 +60,32 @@ def formRegEntry(username, password, nickname):
 
 def getAttributes(base):
        """ This function should, given a spool domish.Element, pull the username, password,
-       and nickname out of it and return them """
+       and out of it and return them """
        username = ""
        password = ""
-       nickname = ""
        for child in base.elements():
                try:
-                       if(child.name == "username"):
+                       if child.name == "username":
                                username = child.__str__()
-                       elif(child.name == "password"):
+                       elif child.name == "password":
                                password = child.__str__()
-                       elif(child.name == "nick"):
-                               nickname = child.__str__()
                except AttributeError:
                        continue
        
-       return username, password, nickname
+       return username, password
 
 
+def startStats(statistics):
+       stats = statistics.stats
+       stats["MessageCount"] = 0
+       stats["FailedMessageCount"] = 0
+       stats["AvatarCount"] = 0
+       stats["FailedAvatarCount"] = 0
 
+def updateStats(statistics):
+       stats = statistics.stats
+       stats["AvatarCount"] = msnp2p.MSNP2P_Avatar.TRANSFER_COUNT
+       stats["FailedAvatarCount"] = msnp2p.MSNP2P_Avatar.ERROR_COUNT
 
 
 def msn2jid(msnid):
@@ -89,31 +101,31 @@ def jid2msn(jid):
 
 def presence2state(show, ptype): 
        """ Converts a Jabber presence into an MSN status code """
-       if(ptype == "unavailable"):
+       if ptype == "unavailable":
                return msn.STATUS_OFFLINE
-       elif(show in [None, "online", "chat"]):
+       elif not show or show == "online" or show == "chat":
                return msn.STATUS_ONLINE
-       elif(show in ["dnd"]):
+       elif show == "dnd":
                return msn.STATUS_BUSY
-       elif(show in ["away", "xa"]):
+       elif show == "away" or show == "xa":
                return msn.STATUS_AWAY
 
 
 def state2presence(state):
        """ Converts a MSN status code into a Jabber presence """
-       if(state == msn.STATUS_ONLINE):
+       if state == msn.STATUS_ONLINE:
                return (None, None)
-       elif(state == msn.STATUS_BUSY):
+       elif state == msn.STATUS_BUSY:
                return ("dnd", None)
-       elif(state == msn.STATUS_AWAY):
+       elif state == msn.STATUS_AWAY:
                return ("away", None)
-       elif(state == msn.STATUS_IDLE):
+       elif state == msn.STATUS_IDLE:
                return ("away", None)
-       elif(state == msn.STATUS_BRB):
+       elif state == msn.STATUS_BRB:
                return ("away", None)
-       elif(state == msn.STATUS_PHONE):
+       elif state == msn.STATUS_PHONE:
                return ("dnd", None)
-       elif(state == msn.STATUS_LUNCH):
+       elif state == msn.STATUS_LUNCH:
                return ("away", None)
        else:
                return (None, "unavailable")
@@ -130,28 +142,28 @@ class LegacyGroupchat(groupchat.BaseGroupchat):
                        - User invited to an existing switchboard session with more than one user
                """
                groupchat.BaseGroupchat.__init__(self, session, resource, ID)
-               if(not existing):
+               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()))
+               LogEvent(INFO, self.roomJID())
        
        def removeMe(self):
                self.switchboardSession.removeMe()
                self.switchboardSession = None
                groupchat.BaseGroupchat.removeMe(self)
-               debug.log("LegacyGroupchat: \"%s\" destroyed" % (self.roomJID()))
+               LogEvent(INFO, self.roomJID())
                utils.mutilateMe(self)
        
        def sendLegacyMessage(self, message, noerror):
-               debug.log("LegacyGroupchat: \"%s\" sendLegacyMessage(\"%s\")" % (self.roomJID(), message))
-               self.switchboardSession.sendMessage(message, noerror)
+               LogEvent(INFO, self.roomJID())
+               self.switchboardSession.sendMessage(message.replace("\n", "\r\n"), noerror)
        
        def sendContactInvite(self, contactJID):
-               debug.log("LegacyGroupchat: \"%s\" sendContactInvite(\"%s\")" % (self.roomJID(), contactJID))
+               LogEvent(INFO, self.roomJID())
                userHandle = jid2msn(contactJID)
                self.switchboardSession.inviteUser(userHandle)
 
@@ -165,10 +177,9 @@ class LegacyConnection(msnw.MSNConnection):
                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))
+               self.remoteShow = ""
+               self.remoteStatus = ""
+               self.remoteNick = ""
 
                # Init the MSN bits
                msnw.MSNConnection.__init__(self, username, password)
@@ -181,42 +192,26 @@ class LegacyConnection(msnw.MSNConnection):
                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)
+               import legacylist # Is in here to prevent an ImportError loop
+               self.legacyList = legacylist.LegacyList(self.session)
        
-               debug.log("LegacyConnection: \"%s\" - created" % (self.session.jabberID))
+               LogEvent(INFO, self.session.jabberID)
        
        def removeMe(self):
-               debug.log("LegacyConnection: \"%s\" - being deleted" % (self.session.jabberID))
+               LogEvent(INFO, 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.legacyList.removeMe()
+               self.legacyList = None
                self.session = None
 
                utils.mutilateMe(self)
        
        def jidRes(self, resource):
                to = self.session.jabberID
-               if(resource):
+               if resource:
                        to += "/" + resource
 
                return to
@@ -227,32 +222,18 @@ class LegacyConnection(msnw.MSNConnection):
        
        def sendMessage(self, dest, resource, body, noerror):
                dest = jid2msn(dest)
-               if(self.userTyping.has_key(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
+               try:
+                       msnw.MSNConnection.sendMessage(self, dest, resource, body, noerror)
+                       self.session.pytrans.statistics.stats["MessageCount"] += 1
+               except:
+                       self.failedMessage(dest, body)
+                       raise
        
        def msnAlert(self, text, actionurl, subscrurl):
+               if not self.session: return
+
                el = Element((None, "message"))
                el.attributes["to"] = self.session.jabberID
                el.attributes["from"] = config.jid
@@ -272,144 +253,184 @@ class LegacyConnection(msnw.MSNConnection):
 
                self.session.pytrans.send(el)
        
-       def setStatus(self, show, status):
+       def setStatus(self, nickname, show, status):
                statusCode = presence2state(show, None)
-               msnw.MSNConnection.changeStatus(self, statusCode, self.buildFriendly(status))
-       
-       def newResourceOnline(self, resource):
-               self.sendLists(resource)
+               msnw.MSNConnection.changeStatus(self, statusCode, nickname, status)
        
-       def jabberSubscriptionReceived(self, source, subtype):
-               self.subscriptions.jabberSubscriptionReceived(source, subtype)
+       def updateAvatar(self, av=None):
+               global defaultJabberAvatarData
+
+               if av:
+                       msnw.MSNConnection.changeAvatar(self, av.getImageData())
+               else:
+                       msnw.MSNConnection.changeAvatar(self, defaultJabberAvatarData)
        
        def sendTypingNotifications(self):
+               if not self.session: return
+       
                # Send any typing notification messages to the user's contacts
                for contact in self.userTyping.keys():
-                       if(self.userTyping[contact]):
+                       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):
+                       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):
+               if not self.session: return
                # Check if the contact has only just started typing
-               if(not self.contactTyping.has_key((contact, resource))):
+               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):
+               if not self.session: return
                dest = jid2msn(dest)
                self.userTyping[dest] = composing
-               if(composing): # Make it instant
+               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()
+               if not self.session: return
+               self.session.sendPresence(to=self.session.jabberID, fro=config.jid)
+               self.legacyList.syncJabberLegacyLists()
+               self.listSynced = True
+               #self.legacyList.flushSubscriptionBuffer()
        
        def gotMessage(self, remoteUser, resource, text):
+               if not self.session: return
                source = msn2jid(remoteUser)
                self.session.sendMessage(self.jidRes(resource), fro=source, body=text, mtype="chat")
+               self.session.pytrans.statistics.stats["MessageCount"] += 1
+       
+       def avatarHashChanged(self, userHandle, hash):
+               if not self.session: return
+               av = self.session.pytrans.avatarCache.getAvatar(hash)
+               if av:
+                       msnContact = self.getContacts().getContact(userHandle)
+                       msnContact.msnobjGot = True
+                       jid = msn2jid(userHandle)
+                       c = self.session.contactList.findContact(jid)
+                       if not c: return
+                       c.updateAvatar(av)
+               else:
+                       self.requestAvatar(userHandle)
+       
+       def gotAvatarImage(self, userHandle, imageData):
+               if not self.session: return
+               jid = msn2jid(userHandle)
+               c = self.session.contactList.findContact(jid)
+               if not c: return
+               av = self.session.pytrans.avatarCache.setAvatar(imageData)
+               c.updateAvatar(av)
        
        def loggedIn(self):
-               if(self.session):
-                       debug.log("LegacyConnection: \"%s\" - loggedIn()" % (self.session.jabberID))
-                       self.session.ready = True
+               if not self.session: return
+               LogEvent(INFO, 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)
+               if not (self.session and self.getContacts()): return
+               LogEvent(INFO, self.session.jabberID)
+               
+               msnContact = self.getContacts().getContact(remoteUser)
+               c = self.session.contactList.findContact(msn2jid(remoteUser))
+               if not (c and msnContact): return
+
+               show, ptype = state2presence(msnContact.status)
+               status = msnContact.personal.decode("utf-8")
+               screenName = msnContact.screenName.decode("utf-8")
+
+               c.updateNickname(screenName, push=False)
+               c.updatePresence(show, status, ptype, force=True)
        
        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)
+               if not self.session: return
+               LogEvent(INFO, self.session.jabberID)
+               self.remoteShow, ptype = state2presence(statusCode)
+               self.sendShowStatus()
+       
+       def ourPersonalChanged(self, statusMessage):
+               if not self.session: return
+               LogEvent(INFO, self.session.jabberID)
+               self.remoteStatus = statusMessage
+               self.sendShowStatus()
+       
+       def ourNickChanged(self, nick):
+               if not self.session: return
+               LogEvent(INFO, self.session.jabberID)
+               self.remoteNick = nick
+               self.sendShowStatus()
+       
+       def sendShowStatus(self):
+               if not self.session: return
+               source = config.jid
+               to = self.session.jabberID
+               self.session.sendPresence(to=to, fro=source, show=self.remoteShow, status=self.remoteStatus, nickname=self.remoteNick)
        
        def userMapping(self, passport, jid):
+               if not self.session: return
                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)
+               if not self.session: return
+               self.session.contactList.getContact(msn2jid(userHandle)).contactRequestsAuth()
        
        def userRemovedMe(self, userHandle):
-               self.subscriptions.msnContactRemovedMe(userHandle)
+               if not self.session: return
+               c = self.session.contactList.getContact(msn2jid(userHandle))
+               c.contactDerequestsAuth()
+               c.contactRemovesAuth()
        
        def serverGoingDown(self):
-               if(self.session):
-                       self.session.sendMessage(to=self.session.jabberID, fro=config.jid, body=lang.get(self.session.lang).msnMaintenance)
+               if not self.session: return
+               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()
+               if not self.session: return
+               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)
+               if not self.session: return
+               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")
+               if not self.session: return
+               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")
+               self.session.removeMe()
        
        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)
+               if not self.session: return
+               self.session.pytrans.statistics.stats["FailedMessageCount"] += 1
+               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):
+               if not self.session: return
                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):
+               if not self.session: return
                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
+               if not self.session: return
+               LogEvent(INFO, self.jabberID)
+               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/legacylist.py b/src/legacy/legacylist.py
new file mode 100644 (file)
index 0000000..3e8c8a0
--- /dev/null
@@ -0,0 +1,187 @@
+# Copyright 2004-2005 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
+from debug import LogEvent, INFO, WARN, ERROR
+import avatar
+import disco
+
+
+# Default avatar for MSN contacts
+f = open("src/legacy/defaultAvatar.png")
+defaultAvatarData = f.read()
+f.close()
+defaultAvatar = avatar.AvatarCache().setAvatar(defaultAvatarData)
+
+
+def getGroupNames(msnContact, msnContactList):
+       groups = []
+       for groupGUID in msnContact.groups:
+               try:
+                       groups.append(msnContactList.groups[groupGUID])
+               except KeyError:
+                       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 LegacyList:
+       def __init__(self, session):
+               self.session = session
+               self.subscriptionBuffer = []
+       
+       def removeMe(self):
+               self.subscriptionBuffer = None
+               self.session = None
+
+       def addContact(self, jid):
+               LogEvent(INFO, self.session.jabberID)
+               userHandle = glue.jid2msn(jid)
+               self.session.legacycon.addContact(msn.FORWARD_LIST, userHandle)
+               self.session.contactList.getContact(jid).contactGrantsAuth()
+       
+       def removeContact(self, jid):
+               LogEvent(INFO, self.session.jabberID)
+               jid = glue.jid2msn(jid)
+               self.session.legacycon.remContact(msn.FORWARD_LIST, jid)
+       
+       
+       def authContact(self, jid):
+               LogEvent(INFO, self.session.jabberID)
+               jid = glue.jid2msn(jid)
+               d = self.session.legacycon.remContact(msn.PENDING_LIST, jid)
+               if d:
+                       self.session.legacycon.addContact(msn.REVERSE_LIST, jid)
+               self.session.legacycon.remContact(msn.BLOCK_LIST, jid)
+               self.session.legacycon.addContact(msn.ALLOW_LIST, jid)
+       
+       def deauthContact(self, jid):
+               LogEvent(INFO, self.session.jabberID)
+               jid = glue.jid2msn(jid)
+               self.session.legacycon.remContact(msn.ALLOW_LIST, jid)
+               self.session.legacycon.addContact(msn.BLOCK_LIST, jid)
+
+
+
+       def syncJabberLegacyLists(self):
+               """ Synchronises the MSN contact list on server with the Jabber contact list """
+
+               global defaultAvatar
+
+               # 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
+               LogEvent(INFO, self.session.jabberID, "Start.")
+               result = self.session.pytrans.xdb.request(self.session.jabberID, disco.IQROSTER)
+               oldContactList = msn.MSNContactList()
+               if result:
+                       for item in result.elements():
+                               user = item.getAttribute("jid")
+                               sub = item.getAttribute("subscription")
+                               lists = item.getAttribute("lists")
+                               if not lists:
+                                       lists = jabsub2msnlist(sub) # Backwards compatible
+                               lists = int(lists)
+                               contact = msn.MSNContact(userHandle=user, screenName="", lists=lists)
+                               oldContactList.addContact(contact)
+               
+               newXDB = Element((None, "query"))
+               newXDB.attributes["xmlns"] = disco.IQROSTER
+               
+               contactList = self.session.legacycon.getContacts()
+
+
+               # Convienence functions
+               def addedToList(num):
+                       return (not (oldLists & num) and (lists & num))
+               def removedFromList(num):
+                       return ((oldLists & num) and not (lists & num))
+               
+               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
+                       
+                       # Create the Jabber representation of the
+                       # contact base on the old list data and then
+                       # sync it with current
+                       jabContact = self.session.contactList.createContact(glue.msn2jid(contact.userHandle), msnlist2jabsub(oldLists))
+                       jabContact.updateAvatar(defaultAvatar, push=False)
+
+                       if addedToList(msn.FORWARD_LIST):
+                               jabContact.syncGroups(getGroupNames(contact, contactList), push=False)
+                               jabContact.syncContactGrantedAuth()
+
+                       if removedFromList(msn.FORWARD_LIST):
+                               jabContact.syncContactRemovedAuth()
+
+                       if addedToList(msn.ALLOW_LIST):
+                               jabContact.syncUserGrantedAuth()
+
+                       if addedToList(msn.BLOCK_LIST) or removedFromList(msn.ALLOW_LIST):
+                               jabContact.syncUserRemovedAuth()
+
+                       if (not (lists & msn.ALLOW_LIST) and not (lists & msn.BLOCK_LIST) and (lists & msn.REVERSE_LIST)) or (lists & msn.PENDING_LIST):
+                               jabContact.contactRequestsAuth()
+
+                       if removedFromList(msn.REVERSE_LIST):
+                               jabContact.contactDerequestsAuth()
+                       
+                       item = newXDB.addElement("item")
+                       item.attributes["jid"] = contact.userHandle
+                       item.attributes["subscription"] = msnlist2jabsub(lists)
+                       item.attributes["lists"] = str(lists)
+               
+               # Update the XDB
+               self.session.pytrans.xdb.set(self.session.jabberID, disco.IQROSTER, newXDB)
+               LogEvent(INFO, self.session.jabberID, "End.")
+       
+       def saveLegacyList(self):
+               contactList = self.session.legacycon.getContacts()
+               if not contactList: return
+
+               newXDB = Element((None, "query"))
+               newXDB.attributes["xmlns"] = disco.IQROSTER
+       
+               for contact in contactList.contacts.values():
+                       item = newXDB.addElement("item")
+                       item.attributes["jid"] = contact.userHandle
+                       item.attributes["subscription"] = msnlist2jabsub(contact.lists) # Backwards compat
+                       item.attributes["lists"] = str(contact.lists)
+
+               self.session.pytrans.xdb.set(self.session.jabberID, disco.IQROSTER, newXDB)
+               LogEvent(INFO, self.session.jabberID, "Finished saving list.")
+       
+
index 85b9256f4c45ca980b1cb891c4e9384cb49f5db5..0644246d30ddc1f52d04829744f2f7333aa0a37e 100644 (file)
@@ -1,14 +1,20 @@
-# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Copyright 2004-2005 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 twisted.python import log
+from debug import LogEvent, INFO, WARN, ERROR
 from tlib import msn
+import math
+import base64
+import binascii
+import math
 import config
 import utils
-import debug
-
+import lang
 
+MAXMESSAGESIZE = 1400
 
 
 class MSNConnection:
@@ -19,10 +25,10 @@ class MSNConnection:
                self.inited = False
                self.tries = 0
                self.initMe()
-               debug.log("MSNConnection: \"%s\" created" % (self.username))
+               LogEvent(INFO, self.session.jabberID)
                
        def initMe(self):
-               if(self.inited):
+               if self.inited:
                        MSNConnection.removeMe(self)
                
                self.switchboardSessions = {}
@@ -36,48 +42,61 @@ class MSNConnection:
                self.notificationFactory.initialListVersion = self.initialListVersion
                self.notificationProtocol = None
                
-               self.savedStatus = None
+               self.savedEvents = SavedEvents()
                
                self.inited = True
                
-               debug.log("MSNConnection: \"%s\" initialised" % (self.username))
+               LogEvent(INFO, self.session.jabberID)
        
        def removeMe(self):
-               debug.log("MSNConnection: \"%s\" destroyed" % (self.username))
-               if(self.notificationProtocol):
+               LogEvent(INFO, self.session.jabberID)
+               if self.notificationProtocol:
                        self.notificationProtocol.removeMe()
-               if(self.notificationFactory):
+               if self.notificationFactory:
                        self.notificationFactory.msncon = None
                self.notificationFactory = None
                self.notificationProtocol = None
-               for userHandle in utils.copyDict(self.switchboardSessions):
+               self.savedEvents = SavedEvents()
+               for userHandle in self.switchboardSessions.copy():
                        self.switchboardSessions[userHandle].removeMe()
                self.switchboardSessions = {}
        
        def resourceOffline(self, offlineResource):
                for contact in self.switchboardSessions.keys():
-                       if(self.switchboardSessions[contact].resource == offlineResource):
+                       if self.switchboardSessions[contact].resource == offlineResource:
                                self.switchboardSessions[contact].resource = self.highestResource()
        
        def getContacts(self):
-               if(self.notificationFactory):
+               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)):
+               LogEvent(INFO, self.session.jabberID)
+               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):
+               elif not noerror:
                        self.failedMessage(remoteUser, text)
        
+       def requestAvatar(self, userHandle):
+               LogEvent(INFO, self.session.jabberID)
+               resource = self.session.highestResource()
+               if config.getAllAvatars:
+                       if not self.switchboardSessions.has_key(userHandle):
+                               self.switchboardSessions[userHandle] = SwitchboardSession(self, userHandle, resource)
+                       else:
+                               self.switchboardSessions[userHandle].requestAvatar()
+               else:
+                       if self.switchboardSessions.has_key(userHandle): # Only request avatars for open switchboards
+                               self.switchboardSessions[userHandle].requestAvatar()
+       
        def sendTypingToContact(self, remoteUser):
-               if(self.switchboardSessions.has_key(remoteUser)):
+               if self.switchboardSessions.has_key(remoteUser):
                        self.switchboardSessions[remoteUser].sendTypingNofication()
        
        def notificationProtocolReady(self, notificationProtocol):
@@ -85,42 +104,61 @@ class MSNConnection:
                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 sendSavedEvents(self):
+               # Hack for events sent before we're logged in
+               self.savedEvents.send(self)
+               self.savedEvents = None
+       
+       def changeAvatar(self, imageData):
+               if self.notificationProtocol:
+                       self.notificationProtocol.changeAvatar(imageData, push=True)
+               else:
+                       self.savedEvents.avatarImageData = imageData
        
-       def changeStatus(self, statusCode, screenName):
-               if(self.notificationProtocol):
+       def changeStatus(self, statusCode, screenName, personal):
+               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):
+                       def cb3(arg):
+                               self.ourPersonalChanged(personal)
+                       LogEvent(INFO, self.session.jabberID)
+                       if statusCode:
                                statusCode = str(statusCode.encode("utf-8"))
                                self.notificationProtocol.changeStatus(statusCode).addCallback(cb1)
-                       if(screenName):
+                       if screenName:
                                screenName = str(screenName.encode("utf-8"))
                                self.notificationProtocol.changeScreenName(screenName).addCallback(cb2)
+                       if personal:
+                               personal = str(personal.encode("utf-8"))
+                       else:
+                               personal = ""
+                       self.notificationProtocol.changePersonalMessage(personal).addCallback(cb3)
                else:
-                       self.savedStatus = (statusCode, screenName)
+                       self.savedEvents.statusCode = statusCode
+                       self.savedEvents.screenName = screenName
+                       self.savedEvents.personal = personal
        
        def connectionLostBase(self, reason):
                # Attempts to reconnect
-               if(self.tries < 5 and self.session):
+               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))
+               if self.notificationProtocol:
+                       return self.notificationProtocol.addContact(listType, str(userHandle))
+               else:
+                       self.savedEvents.addContacts.append((listType, str(userHandle)))
        
        def remContact(self, listType, userHandle, groupID=0):
-               return self.notificationProtocol.remContact(listType, str(userHandle))
+               if self.notificationProtocol:
+                       return self.notificationProtocol.remContact(listType, str(userHandle))
+               else:
+                       self.savedEvents.remContacts.append((listType, str(userHandle)))
        
        
        
@@ -142,6 +180,12 @@ class MSNConnection:
        def gotMessage(self, remoteUser, resource, text):
                pass
        
+       def avatarHashChanged(self, userHandle, hash):
+               pass
+       
+       def gotAvatarImage(self, to, image):
+               pass
+       
        def listSynchronized(self):
                pass
        
@@ -151,13 +195,16 @@ class MSNConnection:
        def ourStatusChanged(self, statusCode):
                pass
        
-       def userMapping(self, passport, jid):
+       def ourNickChanged(self, nick):
                pass
        
-       def gotContactTyping(self, remoteUser, resource):
+       def ourPersonalChanged(self, personal):
+               pass
+       
+       def userMapping(self, passport, jid):
                pass
        
-       def ourNickChanged(self, arg):
+       def gotContactTyping(self, remoteUser, resource):
                pass
        
        def serverGoingDown(self):
@@ -180,6 +227,26 @@ class MSNConnection:
 
 
 
+class SavedEvents:
+       def __init__(self):
+               self.nickname = ""
+               self.statusCode = ""
+               self.personal = ""
+               self.avatarImageData = ""
+               self.addContacts = []
+               self.remContacts = []
+       
+       def send(self, msncon):
+               if self.avatarImageData:
+                       msncon.notificationProtocol.changeAvatar(self.avatarImageData, push=False)
+               if self.nickname or self.statusCode or self.personal:
+                       msncon.changeStatus(self.statusCode, self.nickname, self.personal)
+               for listType, userHandle in self.addContacts:
+                       msncon.addContact(listType, userHandle)
+               for listType, userHandle in self.remContacts:
+                       msncon.remContact(listType, userHandle)
+                       
+
 
 def switchToGroupchat(switchboardSession, user1, user2):
        gcsbs = GroupchatSwitchboardSession()
@@ -194,43 +261,107 @@ def switchToGroupchat(switchboardSession, user1, user2):
        gcsbs.userJoined(user2)
        groupchat.sendUserInvite(msn2jid(switchboardSession.remoteUser))
        switchboardSession.removeMe(False)
-       debug.log("GroupchatSwitchboardSession: \"%s\" \"%s\" created by conversion" % (gcsbs.groupchat.roomJID(), gcsbs))
+       LogEvent(INFO, gcsbs.msncon.session.jabberID)
        return gcsbs
 
 
-class GroupchatSwitchboardSession:
+class SwitchboardSessionBase:
+       def sendMessage(self, message, noerror):
+               if self.ready:
+                       def failedMessage(ignored):
+                               if self.__class__ == GroupchatSwitchboardSession:
+                                       tempmsncon.failedMessage(self.groupchat.roomJID(), message)
+                               else:
+                                       tempmsncon.failedMessage(self.remoteUser, message)
+                                       
+                       tempmsncon = self.msncon # In case MSN tells us the message failed after removeMe()
+
+                       LogEvent(INFO, self.ident)
+                       message = str(message.encode("utf-8"))
+                       
+                       if len(message) < MAXMESSAGESIZE:
+                               msnmessage = msn.MSNMessage(message=message)
+                               msnmessage.setHeader("Content-Type", "text/plain; charset=UTF-8")
+                               msnmessage.ack = msn.MSNMessage.MESSAGE_NACK
+
+                               d = self.switchboard.sendMessage(msnmessage)
+                               if not noerror:
+                                       d.addCallback(failedMessage)
+                       else:
+                               chunks = int(math.ceil(len(message) / float(MAXMESSAGESIZE)))
+                               chunk = 0
+                               guid = utils.random_guid()
+                               while chunk < chunks:
+                                       offset = chunk * MAXMESSAGESIZE
+                                       text = message[offset : offset + MAXMESSAGESIZE]
+
+                                       msnmessage = msn.MSNMessage(message=text)
+                                       msnmessage.setHeader("Message-ID", guid)
+                                       if chunk == 0:
+                                               msnmessage.setHeader("Content-Type", "text/plain; charset=UTF-8")
+                                               msnmessage.setHeader("Chunks", str(chunks))
+                                       else:
+                                               msnmessage.setHeader("Chunk", str(chunk))
+                                       msnmessage.ack = msn.MSNMessage.MESSAGE_NACK
+
+                                       d = self.switchboard.sendMessage(msnmessage)
+                                       if not noerror:
+                                               d.addCallback(failedMessage)
+                                       chunk += 1
+
+                       self.resetTimer()
+               else:
+                       self.messageBuffer.append((message, noerror))
+       
+       def gotAvatarImage(self, to, image):
+               self.msncon.gotAvatarImage(to, image)
+
+       def switchboardReady(self, switchboard):
+               LogEvent(INFO, self.ident)
+               self.ready = True
+               self.switchboard = switchboard
+               self.flushBuffer()
+       
+       def resetTimer(self):
+               pass
+
+
+class GroupchatSwitchboardSession(SwitchboardSessionBase):
        def __init__(self, groupchat=None, makeSwitchboard=False):
                self.removed = False
 
                self.msncon = None
-               self.groupchat = None
-               if(groupchat):
+               if groupchat:
+                       self.ident = groupchat.roomJID()
                        self.groupchat = groupchat
                        self.msncon = self.groupchat.session.legacycon
+               else:
+                       self.ident = str(self)
+                       self.groupchat = None
                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))
+               if makeSwitchboard and groupchat:
+                       LogEvent(INFO, self.ident, "Requesting switchboard.")
                        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))
+               if self.msncon:
+                       LogEvent(INFO, self.ident, "Created groupchat for " + self.msncon.username)
        
        def removeMe(self):
-               if(self.removed):
-                       debug.log("GroupchatSwitchboardSession: removeMe called more than once! Traceback!")
+               if self.removed:
+                       log.err("removeMe called more than once!")
                        return
                self.removed = True
 
-               debug.log("GroupchatSwitchboardSession: \"%s\" \"%s\" destroyed" % (self.groupchat.roomJID(), self))
+               LogEvent(INFO, self.ident)
                self.msncon = None
-               if(self.switchboard):
+               if self.switchboard:
                        self.switchboard.removeMe()
                self.switchboard = None
                self.groupchat = None
@@ -240,28 +371,19 @@ class GroupchatSwitchboardSession:
        
        def sbRequestAccepted(self, (host, port, key)):
                # Connect to the switchboard server
-               debug.log("GroupchatSwitchboardSession: \"%s\" \"%s\" sbRequestAccepted()" % (self.msncon.username, self))
+               LogEvent(INFO, self.ident)
                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)
+               if self.oneUserHasJoined:
+                       SwitchboardSessionBase.sendMessage(self, message, noerror)
                else:
-                       self.messageBuffer.append(message)
+                       self.messageBuffer.append((message, noerror))
        
        def inviteUser(self, userHandle):
                userHandle = str(userHandle)
-               if(self.ready):
-                       debug.log("GroupchatSwitchboardSession: \"%s\" \"%s\" inviting %s" % (self.msncon.username, self, userHandle))
+               if self.ready:
+                       LogEvent(INFO, self.ident)
                        self.switchboard.inviteUser(userHandle)
                else:
                        self.invitedUsers.append(userHandle)
@@ -270,30 +392,31 @@ class GroupchatSwitchboardSession:
                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 m, noerror in self.messageBuffer[:]:
+                       self.messageBuffer.remove((m, noerror))
+                       self.sendMessage(m, noerror)
                
-               for i in utils.copyList(self.invitedUsers):
+               for i in 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))
+               LogEvent(INFO, self.ident)
                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))
+               LogEvent(INFO, self.ident)
                self.groupchat.contactLeft(userHandle)
 
 
 
-class SwitchboardSession:
+class SwitchboardSession(SwitchboardSessionBase):
        def __init__(self, msncon, remoteUser, resource, reply=False, host=None, port=None, key=None, sessionID=None):
                self.removed = False
 
+               self.ident = (msncon.session.jabberID, remoteUser)
                self.msncon = msncon
                self.remoteUser = str(remoteUser)
                self.resource = str(resource)
@@ -304,7 +427,7 @@ class SwitchboardSession:
                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):
+               if not reply:
                        # Request a switchboard
                        d = self.msncon.notificationProtocol.requestSwitchboardServer()
                        d.addCallback(self.sbRequestAccepted)
@@ -312,27 +435,27 @@ class SwitchboardSession:
                else:
                        reactor.connectTCP(host, port, SwitchboardFactory(self, key, sessionID, reply))
                
-               debug.log("SwitchboardSession: \"%s\" \"%s\" \"%s\" created" % (self.msncon.username, self.remoteUser, self.resource))
+               LogEvent(INFO, self.ident)
        
        def removeMe(self, sbflag=True):
-               if(self.removed):
-                       debug.log("SwitchboardSession: removeMe called more than once! Traceback!")
+               if self.removed:
+                       log.err("removeMe called more than once!")
                        return
                self.removed = True
 
-               debug.log("SwitchboardSession: \"%s\" \"%s\" \"%s\" destroyed" % (self.msncon.username, self.remoteUser, self.resource))
+               LogEvent(INFO, self.ident)
                for message, noerror in self.messageBuffer:
-                       if(not noerror):
+                       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):
+               if sbflag and self.switchboard:
                        self.switchboard.removeMe()
                self.switchboard = None
                self.ready = False
-               if(self.killTimer and not self.killTimer.called):
+               if self.killTimer and not self.killTimer.called:
                        self.killTimer.cancel()
                self.killTimer = None
 
@@ -347,32 +470,15 @@ class SwitchboardSession:
                # 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):
+               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):
+               for m, noerror in self.messageBuffer[:]:
                        self.messageBuffer.remove((m, noerror))
                        self.sendMessage(m, noerror)
        
@@ -380,20 +486,30 @@ class SwitchboardSession:
                self.msncon.gotMessage(self.remoteUser, self.resource, message.getMessage())
                self.resetTimer()
        
+       CAPS = msn.MSNContact.MSNC1 | msn.MSNContact.MSNC2 | msn.MSNContact.MSNC3 | msn.MSNContact.MSNC4
+       def requestAvatar(self):
+               if not self.switchboard: return
+               msnContacts = self.msncon.getContacts()
+               if not msnContacts: return
+               msnContact = msnContacts.getContact(self.remoteUser)
+               if not (msnContact and msnContact.caps & self.CAPS and msnContact.msnobj): return
+               if msnContact.msnobjGot: return
+               msnContact.msnobjGot = True # This is deliberately set before we get the avatar. So that we don't try to reget failed avatars over & over
+               self.switchboard.sendAvatarRequest(self.remoteUser, msnContact.msnobj)
+       
        def userJoined(self, userHandle):
-               if(userHandle != self.remoteUser):
+               if userHandle != self.remoteUser:
                        # Another user has joined, so we now have three participants (these two and ourself)
                        switchToGroupchat(self, self.remoteUser, userHandle)
+               else:
+                       self.requestAvatar()
        
        def userLeft(self, userHandle):
-               if(userHandle == self.remoteUser):
+               if userHandle == self.remoteUser:
                        self.removeMe()
 
 
 
-
-
-
 class DispatchFactory(ClientFactory):
        def __init__(self, msncon):
                self.msncon = msncon
@@ -402,7 +518,10 @@ class DispatchFactory(ClientFactory):
                p = Dispatch(self.msncon)
                del self.msncon # No longer needed
                return p
-
+       
+       def clientConnectionFailed(self, connector, reason):
+               self.msncon.connectionLostBase(reason)
+       
 
 class Dispatch(msn.DispatchClient):
        def __init__(self, msncon):
@@ -416,7 +535,7 @@ class Dispatch(msn.DispatchClient):
        
        def gotNotificationReferral(self, host, port):
                self.transport.loseConnection()
-               if(self.msncon and self.msncon.session and self.msncon.session.alive):
+               if self.msncon and self.msncon.session and self.msncon.session.alive:
                        reactor.connectTCP(host, port, self.msncon.notificationFactory)
 
 
@@ -425,17 +544,17 @@ class Notification(msn.NotificationClient):
        def __init__(self):
                self.removed = False
 
-               msn.NotificationClient.__init__(self, proxy=config.proxyServer, proxyport=config.proxyPort)
+               msn.NotificationClient.__init__(self)
 
        def removeMe(self):
-               if(self.removed):
-                       debug.log("Notification: removeMe called more than once! Traceback!")
+               if self.removed:
+                       log.err("removeMe called more than once!")
                        return
                self.removed = True
 
                self.logOut()
                self.transport.loseConnection()
-               if(self.factory.msncon):
+               if self.factory.msncon:
                        self.factory.msncon.notificationProtocol = None
                        self.factory.msncon = None
                self.factory = None
@@ -443,167 +562,105 @@ class Notification(msn.NotificationClient):
                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):
+               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
+               if self.badConditions(): return
                self.factory.msncon.loginFailure(message)
        
-       def loggedIn(self, userHandle, screenName, verified):
-               if(self.badConditions()): return
+       def loggedIn(self, userHandle, verified):
+               if self.badConditions(): return
 
                self.factory.msncon.notificationProtocolReady(self)
-               if(not verified):
+               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
+               msn.NotificationClient.loggedIn(self, userHandle, verified)
                
-               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)
-
+               LogEvent(INFO, self.factory.msncon.session.jabberID)
        
+       def msnAlertReceived(self, body, action, subscr):
+               if self.badConditions(): return
+               self.factory.msncon.msnAlert(body, action, subscr)
+
+       def initialEmailNotification(self, inboxunread, foldersunread):
+               if self.badConditions() or not config.mailNotifications: return
+               self.factory.msncon.initialEmailNotification(inboxunread, foldersunread)
+
+       def realtimeEmailNotification(self, mailfrom, fromaddr, subject):
+               if self.badConditions() or not config.mailNotifications: return
+               self.factory.msncon.realtimeEmailNotification(mailfrom, fromaddr, subject)
        
        def connectionLost(self, reason):
-               if(self.badConditions()): return
+               if self.badConditions(): return
                def wait():
-                       debug.log("NotificationClient: \"%s\" lost connection with MSN servers" % (self.factory.userHandle))
+                       LogEvent(INFO, self.factory.msncon.session.jabberID)
                        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))
+               if self.badConditions(): return
+               LogEvent(INFO, self.factory.msncon.session.jabberID)
                self.factory.msncon.listSynchronized()
-               if(self.badConditions()): return # Just in case the session is deregistered
-               self.factory.msncon.sendSavedStatus()
+               if self.badConditions(): return # Just in case the session is deregistered
+               self.factory.msncon.sendSavedEvents()
+               self.setPrivacyMode(False)
        
        def gotSwitchboardInvitation(self, sessionID, host, port, key, remoteUser, screenName):
-               if(self.badConditions()): return
-               debug.log("NotificationClient: \"%s\" gotSwitchboardInvitation(\"%s\")" % (self.factory.userHandle, remoteUser))
+               if self.badConditions(): return
+               LogEvent(INFO, self.factory.msncon.session.jabberID)
                sbs = SwitchboardSession(self.factory.msncon, remoteUser, self.factory.msncon.session.highestResource(), True, host, port, key, sessionID)
-               if(self.factory.msncon.switchboardSessions.has_key(remoteUser)):
+               if self.factory.msncon.switchboardSessions.has_key(remoteUser):
                        self.factory.msncon.switchboardSessions[remoteUser].removeMe()
                self.factory.msncon.switchboardSessions[remoteUser] = sbs
        
+       def avatarHashChanged(self, userHandle, hash):
+               if self.badConditions(): return
+               LogEvent(INFO, self.factory.msncon.session.jabberID)
+               hash = base64.decodestring(hash)
+               hash = binascii.hexlify(hash)
+               self.factory.msncon.avatarHashChanged(userHandle, hash)
+       
        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)
+               if self.badConditions(): return
+               LogEvent(INFO, self.factory.msncon.session.jabberID)
                
                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))
-               
+       def contactPersonalChanged(self, userHandle, personal):
+               if self.badConditions(): return
+               msn.NotificationClient.contactPersonalChanged(self, userHandle, personal)
                self.factory.msncon.contactStatusChanged(userHandle)
        
        def contactOffline(self, userHandle):
-               if(self.badConditions()): return
-               debug.log("NotificationClient: \"%s\" contactOffline(\"%s\")" % (self.factory.userHandle, userHandle))
+               if self.badConditions(): return
+               LogEvent(INFO, self.factory.msncon.session.jabberID)
                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)
+       def userAddedMe(self, userGuid, userHandle, screenName):
+               if self.badConditions(): return
+               LogEvent(INFO, self.factory.msncon.session.jabberID)
+               msn.NotificationClient.userAddedMe(self, userGuid, userHandle, screenName)
                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)
+       def userRemovedMe(self, userGuid, userHandle):
+               if self.badConditions(): return
+               LogEvent(INFO, self.factory.msncon.session.jabberID)
+               msn.NotificationClient.userRemovedMe(self, userGuid, userHandle)
                self.factory.msncon.userRemovedMe(userHandle)
        
        def multipleLogin(self):
-               if(self.badConditions()): return
-               debug.log("NotificationClient: \"%s\" multiple logins" % (self.factory.msncon.username))
+               if self.badConditions(): return
+               LogEvent(INFO, self.factory.msncon.session.jabberID)
                self.factory.msncon.multipleLogin()
 
 
@@ -617,7 +674,7 @@ class SwitchboardFactory(ClientFactory):
        
        def buildProtocol(self, addr):
                p = Switchboard(self.switchboardSession)
-               if(p.badConditions()): return p
+               if p.badConditions(): return p
                p.key = self.key
                p.sessionID = self.sessionID
                p.reply = self.reply
@@ -629,65 +686,62 @@ 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))
+               msn.SwitchboardClient.__init__(self)
+               if self.badConditions(): return
+               self.msnobj = self.switchboardSession.msncon.notificationProtocol.msnobj
+               LogEvent(INFO, self.switchboardSession.ident)
        
        def removeMe(self):
-               if(self.removed):
-                       debug.log("Switchboard: removeMe called more than once! Traceback!")
+               if self.removed:
+                       log.err("removeMe called more than once!")
                        return
                self.removed = True
 
                self.transport.loseConnection()
-               debug.log("SwitchboardClient: \"%s\" - destroyed" % (self.switchboardSession))
+               LogEvent(INFO, self.switchboardSession.ident)
                self.switchboardSession = None
                self.factory.switchboardSession = None
                self.factory = None
 
-               if(self.callid and not self.callid.called):
+               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):
+               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):
+                       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):
+               if self.badConditions(): return
+               if (not self.reply) and self.switchboardSession.__class__ == SwitchboardSession:
                        def failCB(arg=None):
-                               debug.log(templogmessage)
+                               LogEvent(INFO, ident, "User has not joined after 30 seconds.")
                                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)
+                       ident = self.switchboardSession.ident
                        # 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()
+       def readySwitchboardSession(self):
+               self.switchboardSession.switchboardReady(self)
                for user in self.chattingUsers:
                        self.switchboardSession.userJoined(user)
-               if(self.callid and not self.callid.called):
+               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
        
@@ -696,43 +750,49 @@ class Switchboard(msn.SwitchboardClient):
                        self.chattingUsers.append(user)
        
        def userJoined(self, userHandle, screenName):
-               if(self.badConditions()): return
-               if((not self.reply) and self.switchboardSession.__class__ == SwitchboardSession):
+               if self.badConditions(): return
+               # FIXME - check this is correct
+               if (not self.reply) and isinstance(self.switchboardSession, SwitchboardSession):
                        self.readySwitchboardSession()
-               debug.log("SwitchboardClient: \"%s\" \"%s\" - userJoined(\"%s\")" % (self.switchboardSession.msncon.username, self.switchboardSession, userHandle))
+               LogEvent(INFO, self.switchboardSession.ident)
                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))
+               if self.badConditions(): return
+               LogEvent(INFO, self.switchboardSession.ident)
                def wait():
+                       if self.badConditions(): return
                        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!")
+               if self.badConditions():
+                       LogEvent(WARN, self.switchboardSession.ident, "gotMessage() called too late. Dropped a message!")
                        return
-               debug.log("SwitchboardClient: \"%s\" \"%s\" gotMessage(\"%s\")" % (self.switchboardSession.msncon.username, message.userHandle, message.getMessage()))
+
+               LogEvent(INFO, self.switchboardSession.ident)
                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)
+               if "text/plain" in cTypes:
+                       try:
+                               if len(cTypes) > 1 and cTypes[1].find("UTF-8") >= 0:
+                                       message.message = message.message.decode("utf-8")
+                               self.switchboardSession.gotMessage(message)
+                       except:
+                               self.switchboardSession.gotMessage(lang.get(self.switchboardSession.msncon.session.lang).msnDroppedMessage) # FIXME, this is a little deep
+                               raise
                        return
-               if("text/x-clientcaps" in cTypes):
-                       if(message.hasHeader("JabberID")):
+               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()))
+               LogEvent(INFO, self.switchboardSession.ident, "Discarding unknown message type.")
        
        def userTyping(self, message):
-               if(self.badConditions()): return
-               if(self.switchboardSession.__class__ == SwitchboardSession): # Ignore typing in groupchats
-                       if(message.userHandle == self.switchboardSession.remoteUser):
+               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):
@@ -741,5 +801,19 @@ class Switchboard(msn.SwitchboardClient):
                message.setHeader("Client-Name", "PyMSNt")
                message.setHeader("JabberID", str(self.switchboardSession.msncon.session.jabberID)) # FIXME, this is a little deep
                self.sendMessage(message)
+       
+       def sendMessage(self, message):
+               # A little bit of fancyness to make sure that clientcaps
+               # only gets sent after the first text message.
+               if message.getHeader("Content-Type").startswith("text"):
+                       self.sendMessage = type(self.sendMessage)(msn.SwitchboardClient.sendMessage, self, Switchboard)
+                       self.sendClientCaps()
+                       return self.sendMessage(message)
+               else:
+                       return msn.SwitchboardClient.sendMessage(self, message)
+       
+       def gotAvatarImage(self, to, image):
+               if self.badConditions(): return
+               self.switchboardSession.gotAvatarImage(to, image)
 
  
index 701085034abf45116eec78e646dbb0ed10f193e7..70d0733be5de52809e095975653a4e65ad7201c7 100644 (file)
@@ -1,53 +1,40 @@
-# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Copyright 2004-2005 James Bunton <james@delx.cjb.net>
 # Licensed for distribution under the GPL version 2, check COPYING for details
 
 import utils
 import os
+import os.path
 import shutil
-if(os.name == "posix"):
-       import signal
+import time
 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()):
+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
-
+from debug import LogEvent, INFO, WARN, ERROR
 
 import xdb
+import avatar
 import session
 import jabw
 import disco
 import register
 import misciq
 import lang
-import debug
 import legacy
+import housekeep
 
 #import gc
 #gc.set_debug(gc.DEBUG_COLLECTABLE | gc.DEBUG_UNCOLLECTABLE | gc.DEBUG_INSTANCES | gc.DEBUG_OBJECTS)
@@ -55,19 +42,30 @@ import legacy
 
 class PyTransport(component.Service):
        def __init__(self):
-               debug.log("PyTransport: Service starting up")
+               LogEvent(INFO)
+
+               # Do any auto-update stuff
+               housekeep.init()
                
                # 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.discovery.addIdentity("gateway", legacy.id, legacy.name, config.jid)
+               self.discovery.addIdentity("conference", "text", legacy.name + " Chatrooms", config.jid)
+               self.discovery.addFeature(disco.XCONFERENCE, None, config.jid) # So that clients know you can create groupchat rooms on the server
+               self.discovery.addFeature("jabber:iq:conference", None, config.jid) # We don't actually support this, but Psi has a bug where it looks for this instead of the above
                
                self.xdb = xdb.XDB(config.jid, legacy.mangle)
+               self.avatarCache = avatar.AvatarCache()
                self.registermanager = register.RegisterManager(self)
                self.gatewayTranslator = misciq.GatewayTranslator(self)
                self.versionTeller = misciq.VersionTeller(self)
                self.pingService = misciq.PingService(self)
+               self.adHocCommands = misciq.AdHocCommands(self)
+               self.vCardFactory = misciq.VCardFactory(self)
+               self.iqAvatarFactor = misciq.IqAvatarFactory(self)
+               self.connectUsers = misciq.ConnectUsers(self)
+               self.statistics = misciq.Statistics(self)
+               self.startTime = int(time.time())
 
                self.xmlstream = None
                self.sessions = {}
@@ -79,31 +77,13 @@ class PyTransport(component.Service):
                # 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)
-               
-       
+               self.loopCall = task.LoopingCall(self.loopCall)
+               self.loopCall.start(60.0)
+
        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!"
+               LogEvent(INFO)
+               for session in self.sessions.copy():
+                       self.sessions[session].removeMe()
        
        def makeMessageID(self):
                self.messageID += 1
@@ -112,7 +92,7 @@ class PyTransport(component.Service):
        def makeID(self):
                newID = "r" + str(self.lastID)
                self.lastID += 1
-               if(self.reservedIDs.count(newID) > 0):
+               if self.reservedIDs.count(newID) > 0:
                        # Ack, it's already used.. Try again
                        return self.makeID()
                else:
@@ -122,87 +102,113 @@ class PyTransport(component.Service):
                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)
+               numsessions = len(self.sessions)
+
+               #if config.debugOn and numsessions > 0:
+               #       print "Sessions:"
+               #       for key in self.sessions:
+               #               print "\t" + self.sessions[key].jabberID
        
-       def loopCheckSessionsCall(self):
-               if(len(self.sessions) > 0):
-                       oldDict = utils.copyDict(self.sessions)
+               self.statistics.stats["Uptime"] = int(time.time()) - self.startTime
+               self.statistics.stats["OnlineUsers"] = numsessions
+               legacy.updateStats(self.statistics)
+               if numsessions > 0:
+                       oldDict = self.sessions.copy()
                        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))
+                               if not session.alive:
+                                       LogEvent(WARN, "", "Ghost session found.")
                                        # 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")
+               LogEvent(INFO)
                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)
+               if config.useXCP:
+                       pres = Element((None, "presence"))
+                       pres.attributes["to"] = "presence@-internal"
+                       pres.attributes["from"] = config.compjid
+                       x = pres.addElement("x")
+                       x.attributes["xmlns"] = "http://www.jabber.com/schemas/component-presence.xsd"
+                       x.attributes["xmlns:config"] = "http://www.jabber.com/config"
+                       x.attributes["config:version"] = "1"
+                       x.attributes["protocol-version"] = "1.0"
+                       x.attributes["config-ns"] = legacy.url + "/component"
+                       self.send(pres)
        
        def componentDisconnected(self):
-               debug.log("PyTransport: Disconnected from main Jabberd server")
+               LogEvent(INFO)
                self.xmlstream = None
        
        def onRouteMessage(self, el):
                for child in el.elements():
-                       if(child.name == "message"):
+                       if child.name == "message":
                                self.onMessage(child)
-                       elif(child.name == "presence"):
+                       elif child.name == "presence":
+                               # Ignore any presence broadcasts about other XCP components
+                               if child.getAttribute("to") and child.getAttribute("to").find("@-internal") > 0: return
                                self.onPresence(child)
-                       elif(child.name == "iq"):
+                       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
+               try:
+                       froj = jid.JID(fro)
+               except Exception, e:
+                       LogEvent(WARN, "", "Failed stringprep.")
+                       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())):
+               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.")
+               elif mtype != "error":
+                       to = el.getAttribute("to")
+                       ulang = utils.getLang(el)
+                       body = None
+                       for child in el.elements():
+                               if child.name == "body":
+                                       body = child.__str__()
+                       LogEvent(INFO, "", "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())):
+               try:
+                       froj = jid.JID(fro)
+                       toj = jid.JID(to)
+               except Exception, e:
+                       LogEvent(WARN, "", "Failed stringprep.")
+                       return
+
+               if self.sessions.has_key(froj.userhost()):
                        self.sessions[froj.userhost()].onPresence(el)
                else:
-                       if(to.find('@') < 0):
+                       ulang = utils.getLang(el)
+                       ptype = el.getAttribute("type")
+                       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()))
+                               if not el.getAttribute("type"): # Don't create a session unless they're sending available presence
+                                       LogEvent(INFO, "", "Attempting to create a new session.")
                                        s = session.makeSession(self, froj.userhost(), ulang)
-                                       if(s):
+                                       if s:
+                                               self.statistics.stats["TotalUsers"] += 1
                                                self.sessions[froj.userhost()] = s
-                                               debug.log("PyTransport: New session created \"%s\"" % (froj.userhost()))
+                                               LogEvent(INFO, "", "New session created.")
                                                # Send the first presence
                                                s.onPresence(el)
                                        else:
-                                               debug.log("PyTransport: Failed to create session \"%s\"" % (froj.userhost()))
+                                               LogEvent(INFO, "", "Failed to create session")
                                                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()))
+                               elif el.getAttribute("type") != "error":
+                                       LogEvent(INFO, "", "Sending unavailable presence to non-logged in user.")
                                        pres = Element((None, "presence"))
                                        pres.attributes["from"] = to
                                        pres.attributes["to"] = fro
@@ -210,14 +216,14 @@ class PyTransport(component.Service):
                                        self.send(pres)
                                        return
                        
-                       elif(ptype in ["subscribe", "subscribed", "unsubscribe", "unsubscribed"]):
+                       elif ptype and (ptype.startswith("subscribe") or ptype.startswith("unsubscribe")):
                                # 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()))
+                               LogEvent(INFO, "", "Attempting to create a session to do subscription stuff.")
                                s = session.makeSession(self, froj.userhost(), ulang)
-                               if(s):
+                               if s:
                                        self.sessions[froj.userhost()] = s
-                                       debug.log("PyTransport: New session created \"%s\"" % (froj.userhost()))
+                                       LogEvent(INFO, "", "New session created.")
                                        # 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
@@ -226,28 +232,9 @@ class PyTransport(component.Service):
 
 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))
+               jid = config.jid
+               if config.compjid: jid = config.compjid
+               self.c = component.buildServiceManager(jid, config.secret, "tcp:%s:%s" % (config.mainServer, config.port))
                self.transportSvc = PyTransport()
                self.transportSvc.setServiceParent(self.c)
                self.c.startService()
@@ -260,8 +247,9 @@ class App:
        
        def shuttingDown(self):
                self.transportSvc.removeMe()
+               # Keep the transport running for another 3 seconds
                def cb(ignored=None):
-                       os.remove(utils.doPath(config.pid))
+                       pass
                d = Deferred()
                d.addCallback(cb)
                reactor.callLater(3.0, d.callback, None)
@@ -273,53 +261,13 @@ 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__"):
+if os.name == "posix":
+       import signal
        # Set SIGHUP to reload the config file & close & open debug file
-       if(os.name == "posix"):
-               signal.signal(signal.SIGHUP, SIGHUPstuff)
+       signal.signal(signal.SIGHUP, SIGHUPstuff)
 
-       # Check that all the spool files stringprepped
-       doSpoolPrepCheck()
 
+if __name__ == "__main__":
        app = App()
        reactor.run()
 
-
index bbbde0fffea1c765e1c9f9d0709c1f4f0bd729d9..56b5be6158f24b33477f65b97ffbdb723ea2dcb1 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Copyright 2004-2005 James Bunton <james@delx.cjb.net>
 # Licensed for distribution under the GPL version 2, check COPYING for details
 
 import utils
@@ -9,48 +9,361 @@ else:
        from tlib.domish import Element
        from tlib.jabber import jid
 from twisted.internet import reactor, task
-
+from debug import LogEvent, INFO, WARN, ERROR
+import jabw
 import legacy
+import disco
 import config
-import debug
 import lang
+import base64
 import sys
 
 
-class PingService:
+class ConnectUsers:
        def __init__(self, pytrans):
                self.pytrans = pytrans
-               self.pingCounter = 0
-               self.pingCheckTask = task.LoopingCall(self.pingCheck)
-               reactor.callLater(10.0, self.start)
+               self.pytrans.adHocCommands.addCommand("connectusers", self.incomingIq, "command_ConnectUsers")
        
-       def start(self):
-               self.pingCheckTask.start(120.0)
+       def sendProbes(self):
+               for jid in self.pytrans.xdb.files():
+                       jabw.sendPresence(self.pytrans, jid, config.jid, ptype="probe")
        
-       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 incomingIq(self, el):
+               to = el.getAttribute("from")
+               ID = el.getAttribute("id")
+               ulang = utils.getLang(el)
+
+               if config.admins.count(jid.JID(to).userhost()) == 0:
+                       self.pytrans.discovery.sendIqError(to=to, fro=config.jid, ID=ID, xmlns=disco.COMMANDS, etype="cancel", condition="not-authorized")
+                       return
+
+
+               self.sendProbes()
        
-       def pongReceived(self, el):
-               self.pingCounter = 0
+               iq = Element((None, "iq"))
+               iq.attributes["to"] = to
+               iq.attributes["from"] = config.jid
+               if(ID):
+                       iq.attributes["id"] = ID
+               iq.attributes["type"] = "result"
+
+               command = iq.addElement("command")
+               command.attributes["sessionid"] = self.pytrans.makeMessageID()
+               command.attributes["xmlns"] = disco.COMMANDS
+               command.attributes["status"] = "completed"
+
+               x = command.addElement("x")
+               x.attributes["xmlns"] = "jabber:x:data"
+               x.attributes["type"] = "result"
+
+               title = x.addElement("title")
+               title.addContent(lang.get(ulang).command_ConnectUsers)
+
+               field = x.addElement("field")
+               field.attributes["type"] = "fixed"
+               field.addElement("value").addContent(lang.get(ulang).command_Done)
+
+               self.pytrans.send(iq)
+
+
+class Statistics:
+       def __init__(self, pytrans):
+               self.pytrans = pytrans
+               self.pytrans.adHocCommands.addCommand("stats", self.incomingIq, "command_Statistics")
+
+               # self.stats is indexed by a unique ID, with value being the value for that statistic
+               self.stats = {}
+               self.stats["Uptime"] = 0
+               self.stats["OnlineUsers"] = 0
+               self.stats["TotalUsers"] = 0
+
+               legacy.startStats(self)
+
+       def incomingIq(self, el):
+               to = el.getAttribute("from")
+               ID = el.getAttribute("id")
+               ulang = utils.getLang(el)
+
+               iq = Element((None, "iq"))
+               iq.attributes["to"] = to
+               iq.attributes["from"] = config.jid
+               if(ID):
+                       iq.attributes["id"] = ID
+               iq.attributes["type"] = "result"
+
+               command = iq.addElement("command")
+               command.attributes["sessionid"] = self.pytrans.makeMessageID()
+               command.attributes["xmlns"] = disco.COMMANDS
+               command.attributes["status"] = "completed"
+
+               x = command.addElement("x")
+               x.attributes["xmlns"] = "jabber:x:data"
+               x.attributes["type"] = "result"
+
+               title = x.addElement("title")
+               title.addContent(lang.get(ulang).command_Statistics)
+
+               for key in self.stats:
+                       label = getattr(lang.get(ulang), "command_%s" % key)
+                       description = getattr(lang.get(ulang), "command_%s_Desc" % key)
+                       field = x.addElement("field")
+                       field.attributes["var"] = key
+                       field.attributes["label"] = label
+                       field.attributes["type"] = "text-single"
+                       field.addElement("value").addContent(str(self.stats[key]))
+                       field.addElement("desc").addContent(description)
+
+               self.pytrans.send(iq)
+               
+               
+
+class AdHocCommands:
+       def __init__(self, pytrans):
+               self.pytrans = pytrans
+               self.pytrans.discovery.addFeature(disco.COMMANDS, self.incomingIq, config.jid)
+               self.pytrans.discovery.addNode(disco.COMMANDS, self.sendCommandList, "command_CommandList", config.jid, True)
+
+               self.commands = {} # Dict of handlers indexed by node
+               self.commandNames = {} # Dict of names indexed by node
        
-       def makePingPacket(self):
+       def addCommand(self, command, handler, name):
+               self.commands[command] = handler
+               self.commandNames[command] = name
+               self.pytrans.discovery.addNode(command, self.incomingIq, name, config.jid, False)
+       
+       def incomingIq(self, el):
+               itype = el.getAttribute("type")
+               fro = el.getAttribute("from")
+               froj = jid.JID(fro)
+               to = el.getAttribute("to")
+               ID = el.getAttribute("id")
+
+               LogEvent(INFO, "", "Looking for handler")
+
+               node = None
+               for child in el.elements():
+                       xmlns = child.defaultUri
+                       node = child.getAttribute("node")
+
+                       handled = False
+                       if(child.name == "query" and xmlns == disco.DISCO_INFO):
+                               if(node and self.commands.has_key(node) and (itype == "get")):
+                                       self.sendCommandInfoResponse(to=fro, ID=ID)
+                                       handled = True
+                       elif(child.name == "query" and xmlns == disco.DISCO_ITEMS):
+                               if(node and self.commands.has_key(node) and (itype == "get")):
+                                       self.sendCommandItemsResponse(to=fro, ID=ID)
+                                       handled = True
+                       elif(child.name == "command" and xmlns == disco.COMMANDS):
+                               if((node and self.commands.has_key(node)) and (itype == "set" or itype == "error")):
+                                       self.commands[node](el)
+                                       handled = True
+                       if(not handled):
+                               LogEvent(WARN, "", "Unknown Ad-Hoc command received.")
+                               self.pytrans.discovery.sendIqError(to=fro, fro=config.jid, ID=ID, xmlns=xmlns, etype="cancel", condition="feature-not-implemented")
+               
+       
+       def sendCommandList(self, el):
+               to = el.getAttribute("from")
+               ID = el.getAttribute("id")
+               ulang = utils.getLang(el)
+
+               iq = Element((None, "iq"))
+               iq.attributes["to"] = to
+               iq.attributes["from"] = config.jid
+               if ID:
+                       iq.attributes["id"] = ID
+               iq.attributes["type"] = "result"
+
+               query = iq.addElement("query")
+               query.attributes["xmlns"] = disco.DISCO_ITEMS
+               query.attributes["node"] = disco.COMMANDS
+
+               for command in self.commands:
+                       item = query.addElement("item")
+                       item.attributes["jid"] = config.jid
+                       item.attributes["node"] = command
+                       item.attributes["name"] = getattr(lang.get(ulang), self.commandNames[command])
+
+               self.pytrans.send(iq)
+
+       def sendCommandInfoResponse(self, to, ID):
+               LogEvent(INFO, "", "Replying to disco#info")
                iq = Element((None, "iq"))
+               iq.attributes["type"] = "result"
                iq.attributes["from"] = config.jid
-               iq.attributes["to"] = config.mainServerJID
-               iq.attributes["type"] = "get"
+               iq.attributes["to"] = to
+               if(ID): iq.attributes["id"] = ID
                query = iq.addElement("query")
-               query.attributes["xmlns"] = "jabber:iq:version"
-               return iq
+               query.attributes["xmlns"] = disco.DISCO_INFO
+
+               feature = query.addElement("feature")
+               feature.attributes["var"] = disco.COMMANDS
+               self.pytrans.send(iq)
+
+       def sendCommandItemsResponse(self, to, ID):
+               LogEvent(INFO, "", "Replying to disco#items")
+               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.DISCO_ITEMS
+               self.pytrans.send(iq)
+
+
+class VCardFactory:
+       def __init__(self, pytrans):
+               self.pytrans = pytrans
+               self.pytrans.discovery.addFeature("vcard-temp", self.incomingIq, "USER")
+               self.pytrans.discovery.addFeature("vcard-temp", self.incomingIq, config.jid)
+       
+       def incomingIq(self, el):
+               itype = el.getAttribute("type")
+               fro = el.getAttribute("from")
+               froj = jid.JID(fro)
+               to = el.getAttribute("to")
+               ID = el.getAttribute("id")
+               if(itype != "get" and itype != "error"):
+                       self.pytrans.discovery.sendIqError(to=fro, fro=config.jid, ID=ID, xmlns="vcard-temp", etype="cancel", condition="feature-not-implemented")
+                       return
+
+               LogEvent(INFO, "", "Sending vCard")
+
+               toGateway = not (to.find('@') > 0)
+
+               if(not toGateway):
+                       if(not self.pytrans.sessions.has_key(froj.userhost())):
+                               self.pytrans.discovery.sendIqError(to=fro, fro=config.jid, ID=ID, xmlns="vcard-temp", etype="auth", condition="not-authorized")
+                               return
+                       s = self.pytrans.sessions[froj.userhost()]
+                       if(not s.ready):
+                               self.pytrans.discovery.sendIqError(to=fro, fro=config.jid, ID=ID, xmlns="vcard-temp", etype="auth", condition="not-authorized")
+                               return
+               
+                       c = s.contactList.findContact(to)
+                       if(not c):
+                               self.pytrans.discovery.sendIqError(to=fro, fro=config.jid, ID=ID, xmlns="vcard-temp", etype="cancel", condition="recipient-unavailable")
+                               return
+
+
+               iq = Element((None, "iq"))
+               iq.attributes["to"] = fro
+               iq.attributes["from"] = to
+               if ID:
+                       iq.attributes["id"] = ID
+               iq.attributes["type"] = "result"
+               vCard = iq.addElement("vCard")
+               vCard.attributes["xmlns"] = "vcard-temp"
+               if(toGateway):
+                       FN = vCard.addElement("FN")
+                       FN.addContent(legacy.name)
+                       DESC = vCard.addElement("DESC")
+                       DESC.addContent(legacy.name)
+                       URL = vCard.addElement("URL")
+                       URL.addContent(legacy.url)
+               else:
+                       if(c.nickname):
+                               NICKNAME = vCard.addElement("NICKNAME")
+                               NICKNAME.addContent(c.nickname)
+                       if(c.avatar):
+                               PHOTO = c.avatar.makePhotoElement()
+                               vCard.addChild(PHOTO)
+
+               self.pytrans.send(iq)
+               
+class IqAvatarFactory:
+       def __init__(self, pytrans):
+               self.pytrans = pytrans
+               self.pytrans.discovery.addFeature(disco.IQAVATAR, self.incomingIq, "USER")
+               self.pytrans.discovery.addFeature(disco.STORAGEAVATAR, self.incomingIq, "USER")
+
+       def incomingIq(self, el):
+               itype = el.getAttribute("type")
+               fro = el.getAttribute("from")
+               froj = jid.JID(fro)
+               to = el.getAttribute("to")
+               ID = el.getAttribute("id")
+
+               if(itype != "get" and itype != "error"):
+                       self.pytrans.discovery.sendIqError(to=fro, fro=config.jid, ID=ID, xmlns=disco.IQAVATAR, etype="cancel", condition="feature-not-implemented")
+                       return
+
+               LogEvent(INFO, "", "Retrieving avatar")
+
+               if(not self.pytrans.sessions.has_key(froj.userhost())):
+                       self.pytrans.discovery.sendIqError(to=fro, fro=config.jid, ID=ID, xmlns=disco.IQAVATAR, etype="auth", condition="not-authorized")
+                       return
+               s = self.pytrans.sessions[froj.userhost()]
+               if(not s.ready):
+                       self.pytrans.discovery.sendIqError(to=fro, fro=config.jid, ID=ID, xmlns=disco.IQAVATAR, etype="auth", condition="not-authorized")
+                       return
+
+               c = s.contactList.findContact(to)
+               if(not c):
+                       self.pytrans.discovery.sendIqError(to=fro, fro=config.jid, ID=ID, xmlns=disco.IQAVATAR, etype="cancel", condition="recipient-unavailable")
+                       return
+
+               iq = Element((None, "iq"))
+               iq.attributes["to"] = fro
+               iq.attributes["from"] = to
+               if ID:
+                       iq.attributes["id"] = ID
+               iq.attributes["type"] = "result"
+               query = iq.addElement("query")
+               query.attributes["xmlns"] = disco.IQAVATAR
+               if(c.avatar):
+                       DATA = c.avatar.makeDataElement()
+                       query.addChild(DATA)
+
+               self.pytrans.send(iq)
+
+
+
+class PingService:
+       def __init__(self, pytrans):
+               self.pytrans = pytrans
+#              self.pingCounter = 0
+#              self.pingTask = task.LoopingCall(self.pingCheck)
+               self.pingTask = task.LoopingCall(self.whitespace)
+#              reactor.callLater(10.0, self.start)
+       
+#      def start(self):
+#              self.pingTask.start(120.0)
+       
+       def whitespace(self):
+               self.pytrans.send(" ")
+
+#      def pingCheck(self):
+#              if(self.pingCounter >= 2 and self.pytrans.xmlstream): # Two minutes of no response from the main server
+#                      LogEvent(WARN, "", "Disconnecting because the main server has ignored our pings for too long.")
+#                      self.pytrans.xmlstream.transport.loseConnection()
+#              elif(config.mainServerJID):
+#                      d = self.pytrans.discovery.sendIq(self.makePingPacket())
+#                      d.addCallback(self.pongReceived)
+#                      d.addErrback(self.pongFailed)
+#                      self.pingCounter += 1
+       
+#      def pongReceived(self, el):
+#              self.pingCounter = 0
+       
+#      def pongFailed(self, el):
+#              pass
+       
+#      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"] = disco.IQVERSION
+#              return iq
 
 class GatewayTranslator:
        def __init__(self, pytrans):
                self.pytrans = pytrans
-               self.pytrans.discovery.addFeature("jabber:iq:gateway", self.incomingIq)
+               self.pytrans.discovery.addFeature(disco.IQGATEWAY, self.incomingIq, config.jid)
        
        def incomingIq(self, el):
                fro = el.getAttribute("from")
@@ -63,16 +376,17 @@ class GatewayTranslator:
        
        
        def sendPrompt(self, to, ID, ulang):
-               debug.log("GatewayTranslator: Sending translation details for jabber:iq:gateway - user %s %s" % (to, ID))
+               LogEvent(INFO)
                
                iq = Element((None, "iq"))
                
                iq.attributes["type"] = "result"
                iq.attributes["from"] = config.jid
                iq.attributes["to"] = to
-               iq.attributes["id"] = ID
+               if ID:
+                       iq.attributes["id"] = ID
                query = iq.addElement("query")
-               query.attributes["xmlns"] = "jabber:iq:gateway"
+               query.attributes["xmlns"] = disco.IQGATEWAY
                desc = query.addElement("desc")
                desc.addContent(lang.get(ulang).gatewayTranslator)
                prompt = query.addElement("prompt")
@@ -80,7 +394,7 @@ class GatewayTranslator:
                self.pytrans.send(iq)
        
        def sendTranslation(self, to, ID, el):
-               debug.log("GatewayTranslator: Translating account for jabber:iq:gateway - user %s %s" % (to, ID))
+               LogEvent(INFO)
                
                # Find the user's legacy account
                legacyaccount = None
@@ -94,28 +408,31 @@ class GatewayTranslator:
                
                
                if(legacyaccount and len(legacyaccount) > 0):
-                       debug.log("GatewayTranslator: Sending translated account for jabber:iq:gateway - user %s %s" % (to, ID))
+                       LogEvent(INFO, "", "Sending translated account.")
                        iq = Element((None, "iq"))
                        iq.attributes["type"] = "result"
                        iq.attributes["from"] = config.jid
                        iq.attributes["to"] = to
-                       iq.attributes["id"] = ID
+                       if ID:
+                               iq.attributes["id"] = ID
                        query = iq.addElement("query")
-                       query.attributes["xmlns"] = "jabber:iq:gateway"
+                       query.attributes["xmlns"] = disco.IQGATEWAY
                        prompt = query.addElement("prompt")
                        prompt.addContent(legacy.translateAccount(legacyaccount))
                        
                        self.pytrans.send(iq)
                
                else:
-                       self.pytrans.discovery.sendIqNotValid(to, ID, "jabber:iq:gateway")
+                       self.pytrans.discovery.sendIqError(to, ID, disco.IQGATEWAY)
+                       self.pytrans.discovery.sendIqError(to=to, fro=config.jid, ID=ID, xmlns=disco.IQGATEWAY, etype="retry", condition="bad-request")
 
 
 
 class VersionTeller:
        def __init__(self, pytrans):
                self.pytrans = pytrans
-               self.pytrans.discovery.addFeature("jabber:iq:version", self.incomingIq)
+               self.pytrans.discovery.addFeature(disco.IQVERSION, self.incomingIq, config.jid)
+               self.pytrans.discovery.addFeature(disco.IQVERSION, self.incomingIq, "USER")
        
        def incomingIq(self, el):
                eltype = el.getAttribute("type")
@@ -124,21 +441,21 @@ class VersionTeller:
                self.sendVersion(el)
        
        def sendVersion(self, el):
-               debug.log("Discovery: Sending transport version information")
+               LogEvent(INFO)
                iq = Element((None, "iq"))
                iq.attributes["type"] = "result"
-               iq.attributes["from"] = config.jid
+               iq.attributes["from"] = el.getAttribute("to")
                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"
+               query.attributes["xmlns"] = disco.IQVERSION
                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)
+               os.addContent("Python" + ".".join([str(x) for x in sys.version_info[0:3]]) + " - " + sys.platform)
                
                self.pytrans.send(iq)
 
index 880aef21ddfb33f835e726d2e190e81dc431db3c..dffca0c7feb144836571ef4d4d8f00c5ae12fe08 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Copyright 2004-2005 James Bunton <james@delx.cjb.net>
 # Licensed for distribution under the GPL version 2, check COPYING for details
 
 import utils
@@ -8,25 +8,24 @@ if(utils.checkTwisted()):
 else:
        from tlib.domish import Element
        from tlib.jabber import jid
-
+from debug import LogEvent, INFO, WARN, ERROR
+import disco
 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")
+                       self.pytrans.discovery.addFeature(disco.IQREGISTER, self.incomingRegisterIq, config.jid)
+               LogEvent(INFO)
        
        def removeRegInfo(self, jabberID):
-               debug.log("RegisterManager: removeRegInfo(\"%s\")" % (jabberID))
+               LogEvent(INFO)
                try:
                        # If the session is active then send offline presences
                        session = self.pytrans.sessions[jabberID]
@@ -35,37 +34,37 @@ class RegisterManager:
                        pass
                
                self.pytrans.xdb.remove(jabberID)
-               debug.log("RegisterManager: removeRegInfo(\"%s\") - done" % (jabberID))
+               LogEvent(INFO, "", "done")
        
        
-       def setRegInfo(self, jabberID, username, password, nickname):
-               debug.log("RegisterManager: setRegInfo(\"%s\", \"%s\", \"%s\", \"%s\")" % (jabberID, username, password, nickname))
+       def setRegInfo(self, jabberID, username, password):
+               LogEvent(INFO)
                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)
+               reginfo = legacy.formRegEntry(username, password)
+               self.pytrans.xdb.set(jid.JID(jabberID).userhost(), legacy.namespace, reginfo)
        
        def getRegInfo(self, jabberID):
-               debug.log("RegisterManager: getRegInfo(\"%s\")" % (jabberID))
-               result = self.pytrans.xdb.request(jid.JID(jabberID).full(), legacy.namespace)
+               LogEvent(INFO)
+               result = self.pytrans.xdb.request(jid.JID(jabberID).userhost(), legacy.namespace)
                if(result == None):
-                       debug.log("RegisterManager: getRegInfo(\"%s\") - not registered!" % (jabberID))
+                       LogEvent(INFO, "", "Not registered!")
                        return None
                
-               username, password, nickname = legacy.getAttributes(result)
+               username, password = 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)
+                       LogEvent(INFO, "", "Returning reg info.")
+                       return (username, password)
                else:
-                       debug.log("RegisterManager: getRegInfo(\"%s\") - invalid registration data! %s %s %s" % (jabberID, username, password, utils.latin1(nickname)))
+                       LogEvent(WARN, "", "Registration data corrupted!")
                        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))
+               LogEvent(INFO)
                if(itype == "get"):
                        self.sendRegistrationFields(incoming)
                elif(itype == "set"):
@@ -73,11 +72,14 @@ class RegisterManager:
                
        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")))
+               ID = incoming.getAttribute("id")
+               fro = incoming.getAttribute("from")
+               LogEvent(INFO)
                reply = Element((None, "iq"))
                reply.attributes["from"] = config.jid
-               reply.attributes["to"] = incoming.getAttribute("from")
-               reply.attributes["id"] = incoming.getAttribute("id")
+               reply.attributes["to"] = fro
+               if ID:
+                       reply.attributes["id"] = ID
                reply.attributes["type"] = "result"
                query = reply.addElement("query")
                query.attributes["xmlns"] = "jabber:iq:register"
@@ -86,28 +88,25 @@ class RegisterManager:
                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)
+               result = self.getRegInfo(incoming.getAttribute("from"))
                if(result):
-                       username, password, nickname = result
+                       username, password = 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()
+               # Grab the username, password
+               ID = incoming.getAttribute("id")
+               fro = incoming.getAttribute("from")
+               LogEvent(INFO)
+               source = jid.JID(fro).userhost()
                ulang = utils.getLang(incoming)
                username = None
                password = None
-               nickname = None
                
                for queryFind in incoming.elements():
                        if(queryFind.name == "query"):
@@ -117,37 +116,30 @@ class RegisterManager:
                                                        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))
+                                                       LogEvent(INFO, "", "Unregistering.")
                                                        try:
                                                                self.removeRegInfo(source)
                                                                self.successReply(incoming)
                                                        except:
                                                                self.xdbErrorReply(incoming)
                                                                return
-                                                       debug.log("RegisterManager: Session \"%s\" has been unregistered" % (source))
+                                                       LogEvent(INFO, "", "Unregistered!")
                                                        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")
+                       LogEvent(INFO, "", "Updating XDB")
                        try:
-                               self.setRegInfo(source, username, password, nickname)
-                               debug.log("RegisterManager: Updated XDB successfully")
+                               self.setRegInfo(source, username, password)
+                               LogEvent(INFO, "", "Updated XDB")
                                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")
+                               LogEvent(INFO, "", "Sent a result Iq")
+                               (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:
@@ -158,7 +150,8 @@ class RegisterManager:
                        self.badRequestReply(incoming)
        
        def badRequestReply(self, incoming):
-               debug.log("RegisterManager: Invalid registration data was sent to us. Or the removal failed.")
+               LogEvent(INFO)
+               # Invalid registration data was sent to us. Or the removal failed
                # Send an error Iq
                reply = incoming
                reply.swapAttributeValues("to", "from")
@@ -166,11 +159,12 @@ class RegisterManager:
                error = reply.addElement("error")
                error.attributes["type"] = "modify"
                interror = error.addElement("bad-request")
-               interror["xmlns"] = XMPP_STANZAS
+               interror["xmlns"] = disco.XMPP_STANZAS
                self.pytrans.send(reply)
        
        def xdbErrorReply(self, incoming):
-               debug.log("RegisterManager: Failure in updating XDB or sending result Iq")
+               LogEvent(INFO)
+               # Failure in updating XDB or sending result Iq
                # send an error Iq
                reply = incoming
                reply.swapAttributeValues("to", "from")
@@ -178,7 +172,7 @@ class RegisterManager:
                error = reply.addElement("error")
                error.attributes["type"] = "wait"
                interror = error.addElement("internal-server-error")
-               interror["xmlns"] = XMPP_STANZAS
+               interror["xmlns"] = disco.XMPP_STANZAS
                self.pytrans.send(reply)
        
        def successReply(self, incoming):
index 59b672af53207d8283ffecb33d47f29fe01b1eb3..28b7322fefef24c064b0b2a4e6cf169ef21b2861 100644 (file)
@@ -1,12 +1,18 @@
-# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Copyright 2004-2005 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 contact
+import avatar
 import config
 import lang
+from debug import LogEvent, INFO, WARN, ERROR
+if utils.checkTwisted():
+       from twisted.words.protocols.jabber import jid
+else:
+       from tlib.jabber import jid
 
 
 
@@ -14,14 +20,14 @@ 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")
+       LogEvent(INFO, jabberID)
+       if pytrans.sessions.has_key(jabberID):
+               LogEvent(INFO, jabberID, "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)
+       if result:
+               username, password = result
+               return Session(pytrans, jabberID, username, password, ulang)
        else:
                return None
 
@@ -31,10 +37,10 @@ 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):
+       def __init__(self, pytrans, jabberID, username, password, 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))
+               LogEvent(INFO, jabberID)
                
                self.pytrans = pytrans
                self.alive = True
@@ -42,7 +48,8 @@ class Session(jabw.JabberConnection):
                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.nickname = ""
+               self.avatar = None
                self.lang = ulang
                
                self.show = None
@@ -52,10 +59,15 @@ class Session(jabw.JabberConnection):
                self.groupchats = []
                
                self.legacycon = legacy.LegacyConnection(self.username, self.password, self)
+               self.contactList = contact.ContactList(self)
+               self.contactList.legacyList = self.legacycon.legacyList
                
-               if(config.sessionGreeting):
+               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))
+
+               self.updateNickname("")
+               self.doVCardUpdate()
+               LogEvent(INFO, self.jabberID, "Created!")
        
        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 """
@@ -63,96 +75,157 @@ class Session(jabw.JabberConnection):
                # Delete all objects cleanly
                # Remove this Session object from the pytrans
                
-               debug.log("Session: Removing \"%s\"" % (self.jabberID))
+               LogEvent(INFO, self.jabberID)
                
                # Mark as dead
                self.alive = False
                self.ready = False
                
                # Send offline presence to the user
-               if(self.pytrans):
+               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):
+               if self.legacycon:
                        self.legacycon.removeMe()
                        self.legacycon = None
-               
+
+               if self.contactList:
+                       self.contactList.removeMe()
+                       self.contactList = None
+
                # Remove any groupchats we may be in
-               for groupchat in utils.copyList(self.groupchats):
+               for groupchat in self.groupchats[:]:
                        groupchat.removeMe()
                
-               if(self.pytrans):
+               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))
+               LogEvent(INFO, self.jabberID, "Removed!")
                utils.mutilateMe(self)
        
+       def doVCardUpdate(self):
+               def vCardReceived(el):
+                       if not self.alive: return
+                       LogEvent(INFO, self.jabberID)
+                       vCard = None
+                       for e in el.elements():
+                               if e.name == "vCard" and e.defaultUri == disco.VCARDTEMP:
+                                       vCard = e
+                                       break
+                       else:
+                               self.legacycon.updateAvatar() # Default avatar
+                               return
+                       avatarSet = False
+                       for e in vCard.elements():
+                               if e.name == "NICKNAME":
+                                       self.updateNickname(e.__str__())
+                               if e.name == "PHOTO":
+                                       imageData = avatar.parsePhotoEl(e)
+                                       if not imageData:
+                                               errback() # Possibly it wasn't in a supported format?
+                                       self.avatar = self.pytrans.avatarCache.setAvatar(imageData)
+                                       self.legacycon.updateAvatar(self.avatar)
+                                       avatarSet = True
+                       if not avatarSet:
+                               self.legacycon.updateAvatar() # Default avatar
+
+               def errback(args=None):
+                       LogEvent(INFO, self.jabberID, "Error fetching avatar.")
+                       if self.alive:
+                               self.legacycon.updateAvatar()
+
+               LogEvent(INFO, self.jabberID, "Fetching avatar.")
+               d = self.sendVCardRequest(to=self.jabberID, fro=config.jid)
+               d.addCallback(vCardReceived)
+               d.addErrback(errback)
+       
        def updateNickname(self, nickname):
                self.nickname = nickname
+               if not self.nickname:
+                       j = jid.JID(self.jabberID)
+                       self.nickname = j.user
                self.setStatus(self.show, self.status)
        
        def setStatus(self, show, status):
                self.show = show
                self.status = status
-               self.legacycon.setStatus(show, status)
+               self.legacycon.setStatus(self.nickname, 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):
+               if pos > 0:
                        roomID = to[:pos]
                else:
                        roomID = to
                
                for groupchat in self.groupchats:
-                       if(groupchat.ID == roomID):
+                       if groupchat.ID == roomID:
                                return groupchat
                
                return None
+       
+       def nicknameReceived(self, source, dest, nickname):
+               if dest.find('@') > 0: return # Ignore presence packets sent to users
+               
+               self.updateNickname(nickname)
+       
+       def avatarHashReceived(self, source, dest, avatarHash):
+               if dest.find('@') > 0: return # Ignore presence packets sent to users
+
+               if avatarHash == " ": # Setting no avatar
+                       self.legacycon.updateAvatar() # Default
+               elif (not self.avatar) or (self.avatar and self.avatar.getImageHash() != avatarHash):
+                       imageData = self.pytrans.avatarCache.getAvatar(avatarHash)
+                       if imageData:
+                               self.avatar = avatar.Avatar(imageData) # Stuff in the cache is always PNG
+                               self.legacycon.updateAvatar(self.avatar)
+                       else:
+                               self.doVCardUpdate()
                
        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))
+               if dest == config.jid:
+                       if body.lower().startswith("end"):
+                               LogEvent(INFO, self.jabberID, "Received 'end' request.")
                                self.removeMe()
                        return
                
-               if(not self.ready):
+               if not self.ready:
                        self.sendNotReadyError(source, resource, dest, body)
                        return
                
                # Sends the message to the legacy translator
                groupchat = self.findGroupchat(dest)
-               if(groupchat):
+               if groupchat:
                        # It's for a groupchat
-                       if(destr and len(destr) > 0 and not noerror):
+                       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))
+                               LogEvent(INFO, self.jabberID, "Groupchat.")
                                groupchat.sendMessage(body, noerror)
                else:
-                       debug.log("Session: messageReceived(), passing onto legacycon.sendMessage()")
+                       LogEvent(INFO, self.jabberID, "Message.")
                        self.legacycon.sendMessage(dest, resource, body, noerror)
        
        def inviteReceived(self, source, resource, dest, destr, roomjid):
-               if(not self.ready):
+               if not self.ready:
                        self.sendNotReadyError(source, resource, dest, roomjid)
                        return
-               
-               if(not roomjid.endswith('@' + config.jid)):
+
+               if not roomjid.endswith('@' + config.jid): # Inviting a MSN user to a Jabber chatroom
                        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))
+               if groupchat:
+                       LogEvent(INFO, self.jabberID, "Groupchat invitation.")
                        groupchat.sendContactInvite(dest)
        
        def typingNotificationReceived(self, dest, resource, composing):
@@ -164,30 +237,32 @@ class Session(jabw.JabberConnection):
                # 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):
+               if groupchat:
                        # It's for an existing groupchat
-                       if(ptype == "unavailable"):
+                       if ptype == "unavailable":
                                # Kill the groupchat
-                               debug.log("Session: Presence received to kill groupchat \"%s\" \"%s\"" % (self.jabberID, groupchat.ID))
+                               LogEvent(INFO, self.jabberID, "Killing groupchat.")
                                groupchat.removeMe()
                        else:
-                               if(source == self.jabberID):
-                                       debug.log("Session: Presence for groupchat \"%s\" \"%s\"" % (self.jabberID, groupchat.ID))
-                                       if(ptype == "error"):
+                               if source == self.jabberID:
+                                       LogEvent(INFO, self.jabberID, "Groupchat presence.")
+                                       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))
+                                       LogEvent(INFO, self.jabberID, "Sending groupchat error presence.")
                                        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):
+               elif legacy.isGroupJID(to) and to != config.jid and not ptype:
+                       # Its to a groupchat JID, and the presence type is available
+                       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))
+                       LogEvent(INFO, self.jabberID, "Creating a new groupchat.")
                        groupchat = legacy.LegacyGroupchat(self, resource, gcID) # Creates an empty groupchat
                        groupchat.userJoined(tor)
                
@@ -197,43 +272,45 @@ class Session(jabw.JabberConnection):
 
                
        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
+               if ptype and ptype != "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))
+               if ptype == "unavailable":
+                       if existing:
+                               LogEvent(INFO, self.jabberID, "Resource gone offline.")
                                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)) 
+                       if not existing:
+                               LogEvent(INFO, self.jabberID, "Resource came online.")
+                               self.contactList.resendLists(source + "/" + resource)
+                       LogEvent(INFO, self.jabberID, "Setting status.")
                        self.resourceList[resource] = SessionResource(show, status, priority)
 
                highestActive = self.highestResource()
 
-               if(highestActive):
+               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))
+                       LogEvent(INFO, self.jabberID, "Updating status on legacy service.")
                        r = self.resourceList[highestActive]
                        self.setStatus(r.show, r.status)
                else:
-                       debug.log("Session %s - tearing down, last resource gone offline")
+                       LogEvent(INFO, self.jabberID, "Last resource died. Calling removeMe in 0 seconds.")
+                       #reactor.callLater(0, self.removeMe)
                        self.removeMe()
+                       #FIXME Which of the above?
        
        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)
+                       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))
+#              if highestActive:
+#                      debug.log("Session %s - highest active resource is \"%s\" at %d" % (self.jabberID, highestActive, self.resourceList[highestActive].priority))
 
                return highestActive
                
@@ -244,8 +321,19 @@ class Session(jabw.JabberConnection):
        
        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)
+               if to.find('@') > 0:
+                       LogEvent(INFO, self.jabberID, "Passing subscription to legacy service.")
+                       self.contactList.jabberSubscriptionReceived(to, subtype)
+               else:
+                       if subtype == "subscribe":
+                               self.sendPresence(to=self.jabberID, fro=config.jid, ptype="subscribed")
+                       elif subtype.startswith("unsubscribe"):
+                               # They want to unregister.
+                               jid = self.jabberID
+                               LogEvent(INFO, jid, "About to unregister.")
+                               self.pytrans.registermanager.removeRegInfo(jid)
+                               LogEvent(INFO, jid, "Just unregistered.")
+
        
 
 
index 9dc1e5040f7e55a13de6eec1552cf683adfd5073..c5c9d399fc99df7db2c523a0d547c4833ce48ce2 100644 (file)
@@ -1,5 +1,6 @@
 # Twisted, the Framework of Your Internet
 # Copyright (C) 2001-2002 Matthew W. Lefkowitz
+# Copyright (C) 2004-2005 James C. Bunton
 #
 # 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
 #
 
 """
-MSNP8 Protocol (client only) - semi-experimental
+MSNP11 Protocol (client only) - semi-experimental
 
 Stability: unstable.
 
-This module provides support for clients using the MSN Protocol (MSNP8).
+This module provides support for clients using the MSN Protocol (MSNP11).
 There are basically 3 servers involved in any MSN session:
 
 I{Dispatch server}
@@ -70,7 +71,7 @@ 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
+Due to the lack of an official spec for MSNP11, 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
@@ -96,7 +97,8 @@ if(utils.checkTwisted()):
        from twisted.web.http import HTTPClient
 else:
        from twisted.protocols.http import HTTPClient
-from proxy import proxy_connect_ssl
+import msnp11chl
+import msnp2p
 
 # Twisted imports
 from twisted.internet import reactor, task
@@ -104,17 +106,18 @@ from twisted.internet.defer import Deferred
 from twisted.internet.protocol import ClientFactory
 from twisted.internet.ssl import ClientContextFactory
 from twisted.python import failure, log
+from twisted.xish.domish import unescapeFromXml
 
 # System imports
-import types, operator, os, md5
+import types, operator, os
 from random import randint
 from urllib import quote, unquote
 
-MSN_PROTOCOL_VERSION = "MSNP8 CVR0"       # protocol version
+MSN_PROTOCOL_VERSION = "MSNP11 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" # :(
+MSN_CVR_STR          = "0x040c winnt 5.1 i386 MSNMSGR 7.0.0777 msmsgs"
+MSN_AVATAR_GUID      = "{A4268EEC-FEC5-49E5-95C3-F126696BDBF6}"
 
 # auth constants
 LOGIN_SUCCESS  = 1
@@ -126,12 +129,14 @@ FORWARD_LIST = 1
 ALLOW_LIST   = 2
 BLOCK_LIST   = 4
 REVERSE_LIST = 8
+PENDING_LIST = 16
 
 # phone constants
 HOME_PHONE   = "PHH"
 WORK_PHONE   = "PHW"
 MOBILE_PHONE = "PHM"
 HAS_PAGER    = "MOB"
+HAS_BLOG     = "HSB"
 
 # status constants
 STATUS_ONLINE  = 'NLN'
@@ -149,6 +154,9 @@ LF = "\n"
 
 LINEDEBUG = False
 
+def getVal(inp):
+    return inp.split('=')[1]
+
 def checkParamLen(num, expected, cmd, error=None):
     if error == None: error = "Invalid Number of Parameters for %s" % cmd
     if num != expected: raise MSNProtocolError, error
@@ -181,7 +189,7 @@ def _parsePrimitiveHost(host):
     p = '/' + p
     return h,p
 
-def _login(userHandle, passwd, nexusServer, cached=0, authData='', proxy=None, proxyport=None):
+def _login(userHandle, passwd, nexusServer, cached=0, authData=''):
     """
     This function is used internally and should not ever be called
     directly.
@@ -190,10 +198,7 @@ def _login(userHandle, passwd, nexusServer, cached=0, authData='', proxy=None, p
     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())
+        reactor.connectSSL(_parsePrimitiveHost(server)[0], 443, loginFac, ClientContextFactory())
 
     if cached:
         _cb(nexusServer, authData)
@@ -203,10 +208,7 @@ def _login(userHandle, passwd, nexusServer, cached=0, authData='', proxy=None, p
         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())
+        reactor.connectSSL(_parsePrimitiveHost(nexusServer)[0], 443, fac, ClientContextFactory())
     return cb
 
 
@@ -341,14 +343,16 @@ class MSNMessage:
                If set to MESSAGE_ACK_NONE sendMessage will return None.
     """
     MESSAGE_ACK      = 'A'
+    MESSAGE_ACK_FAT  = 'D'
     MESSAGE_NACK     = 'N'
     MESSAGE_ACK_NONE = 'U'
 
     ack = MESSAGE_ACK
 
-    def __init__(self, length=0, userHandle="", screenName="", message=""):
+    def __init__(self, length=0, userHandle="", screenName="", message="", specialMessage=False):
         self.userHandle = userHandle
         self.screenName = screenName
+        self.specialMessage = specialMessage
         self.message = message
         self.headers = {'MIME-Version' : '1.0', 'Content-Type' : 'text/plain'}
         self.length = length
@@ -389,14 +393,19 @@ class MSNContact:
     """
     This class represents a contact (user).
 
+    @ivar userGuid: The contact's user guid (unique string)
     @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 caps: int, The capabilities of this client
+    @ivar msnobj: msnp2p.MSNOBJ, The MSNObject representing the contact's avatar
     @ivar status: The contact's status code.
     @type status: str if contact's status is known, None otherwise.
+    @ivar personal: The contact's personal message .
+    @type personal: str if contact's personal message is known, None otherwise.
 
     @ivar homePhone: The contact's home phone number.
     @type homePhone: str if known, otherwise None.
@@ -405,21 +414,32 @@ class MSNContact:
     @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
+    @ivar hasBlog: Whether or not this user has a MSN Spaces blog
                     (true=yes, false=no)
     """
+    MSNC1 = 0x10000000
+    MSNC2 = 0x20000000
+    MSNC3 = 0x30000000
+    MSNC4 = 0x40000000
     
-    def __init__(self, userHandle="", screenName="", lists=0, groups=[], status=None):
+    def __init__(self, userGuid="", userHandle="", screenName="", lists=0, caps=0, msnobj=None, groups={}, status=None, personal=""):
+        self.userGuid = userGuid
         self.userHandle = userHandle
         self.screenName = screenName
         self.lists = lists
+        self.caps = caps
+        self.msnobj = msnobj
+        self.msnobjGot = True
         self.groups = [] # if applicable
         self.status = status # current status
+        self.personal = personal
 
         # phone details
         self.homePhone   = None
         self.workPhone   = None
         self.mobilePhone = None
         self.hasPager    = None
+        self.hasBlog     = None
 
     def setPhone(self, phoneType, value):
         """
@@ -432,7 +452,8 @@ class MSNContact:
         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"
+        elif t == HAS_BLOG: self.hasBlog = value
+        #else: raise ValueError, "Invalid Phone Type: " + t
 
     def addToList(self, listType):
         """
@@ -456,7 +477,6 @@ class MSNContactList:
 
     @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
@@ -468,7 +488,6 @@ class MSNContactList:
 
     def __init__(self):
         self.contacts = {}
-        self.version = 0
         self.groups = {}
         self.autoAdd = 0
         self.privacy = 0
@@ -626,16 +645,14 @@ class MSNEventBase(LineReceiver):
             except ValueError:
                 #raise MSNProtocolError, "Invalid Message Header"
                 line = ""
-            if line == "" or self.currentMessage.userHandle == "NOTIFICATION":
+            if line == "" or self.currentMessage.specialMessage:
                 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 = ""
+            raise MSNProtocolError, "Invalid Message, %s" % repr(line)
 
         if len(cmd) != 3: raise MSNProtocolError, "Invalid Command, %s" % repr(cmd)
         if cmd.isdigit():
@@ -672,8 +689,8 @@ class MSNEventBase(LineReceiver):
         if not self.checkMessage(m):
             self.setLineMode(extra)
             return
-        self.setLineMode(extra)
         self.gotMessage(m)
+        self.setLineMode(extra)
 
     ### protocol command handlers - no need to override these.
 
@@ -690,13 +707,6 @@ class MSNEventBase(LineReceiver):
 
     ### 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))
@@ -767,13 +777,14 @@ class NotificationClient(MSNEventBase):
 
     factory = None # sssh pychecker
 
-    def __init__(self, currentID=0, proxy=None, proxyport=None):
+    def __init__(self, currentID=0):
         MSNEventBase.__init__(self)
         self.currentID = currentID
         self._state = ['DISCONNECTED', {}]
-        self.proxy, self.proxyport = proxy, proxyport
         self.pingCounter = 0
         self.pingCheckTask = None
+        self.msnobj = msnp2p.MSNOBJ()
+        self.msnobj.setNull()
 
     def _setState(self, state):
         self._state[0] = state
@@ -803,12 +814,101 @@ class NotificationClient(MSNEventBase):
             self.pingCheckTask = None
         MSNEventBase.connectionLost(self, reason)
 
+    def _getEmailFields(self, message):
+        fields = message.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
+
+    def _gotInitialEmailNotification(self, message):
+        values = self._getEmailFields(message)
+        try:
+            inboxunread = int(values["Inbox-Unread"])
+            foldersunread = int(values["Folders-Unread"])
+        except KeyError:
+            return
+        if(foldersunread + inboxunread == 0): # For some reason MSN sends notifications about empty inboxes sometimes?
+            self.initialEmailNotification(inboxunread, foldersunread)
+
+    def _gotEmailNotification(self, message):
+        values = self._getEmailFields(message)
+        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
+        self.realtimeEmailNotification(mailfrom, fromaddr, subject)
+
+    def _gotMSNAlert(self, message):
+        notification = utils.parseText(message.message, beExtremelyLenient=True)
+        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) # Used to have $siteurl// at the beginning, but it seems to not work with that now. Weird
+        subscrurl = "%s&notification_id=%s&message_id=%s&agent=messenger" % (subscr, notid, msgid)
+
+        self.msnAlertReceived(bodytext, actionurl, subscrurl)
+
+    def _gotUBX(self, message):
+        lm = message.message.lower()
+        p1 = lm.find("<psm>") + 5
+        p2 = lm.find("</psm>")
+        if p1 >= 0 and p2 >= 0:
+            personal = unescapeFromXml(message.message[p1:p2])
+            self.contactPersonalChanged(message.userHandle, personal)
+
     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
+        elif("text/x-msmsgsinitialemailnotification" in cTypes):
+            self._gotInitialEmailNotification(message)
+            return 0
+        elif("text/x-msmsgsemailnotification" in cTypes):
+            self._gotEmailNotification(message)
+            return 0
+        elif("NOTIFICATION" == message.userHandle and message.specialMessage == True):
+            self._gotMSNAlert(message)
+            return 0
+        elif("UBX" == message.screenName and message.specialMessage == True):
+            self._gotUBX(message)
+            return 0
         return 1
 
     ### protocol command handlers - no need to override these
@@ -824,23 +924,23 @@ class NotificationClient(MSNEventBase):
         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:
+        if not (4 <= 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]))
+            self.loggedIn(params[2], int(params[3]))
         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 = _login(f.userHandle, f.password, f.passportServer, authData=params[3])
             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)
+                       result[1], cached=1, authData=result[2])
             d.addCallback(self._passportLogin)
             d.addErrback(self._passportError)
         elif result[0] == LOGIN_SUCCESS:
@@ -852,25 +952,48 @@ class NotificationClient(MSNEventBase):
         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.factory.status = statusCode
             self.statusChanged(params[1])
 
     def handle_ILN(self, params):
-        checkParamLen(len(params), 5, 'ILN')
-        self.gotContactStatus(params[1], params[2], unquote(params[3]))
+        #checkParamLen(len(params), 6, 'ILN')
+        msnContact = self.factory.contacts.getContact(params[2])
+        if not msnContact: return
+        msnContact.status = params[1]
+        msnContact.screenName = unquote(params[3])
+        if len(params) > 4: msnContact.caps = int(params[4])
+        if len(params) > 5: self.handleAvatarHelper(msnContact, params[5])
+        self.contactStatusChanged(params[1], params[2], unquote(params[3]))
+
+    def handleAvatarHelper(self, msnContact, msnobjStr):
+        s = unquote(msnobjStr)
+        msnobj = msnp2p.MSNOBJ()
+        msnobj.parse(s)
+        if not msnContact.msnobj or msnobj.sha1d != msnContact.msnobj.sha1d:
+            if msnp2p.MSNP2P_DEBUG: print "Updated MSNOBJ received!", msnobjStr
+            msnContact.msnobj = msnobj
+            msnContact.msnobjGot = False
+            self.avatarHashChanged(msnContact.userHandle, msnContact.msnobj.sha1d)
 
     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())
+       response = msnp11chl.doChallenge(params[1])
+        self.sendLine("QRY %s %s %s" % (self._nextTransactionID(), msnp11chl.MSNP11_PRODUCT_ID, len(response)))
+        self.transport.write(response)
 
     def handle_QRY(self, params):
         pass
 
     def handle_NLN(self, params):
-        checkParamLen(len(params), 4, 'NLN')
+        if not self.factory: return
+        msnContact = self.factory.contacts.getContact(params[1])
+        if not msnContact: return
+        msnContact.status = params[0]
+        msnContact.screenName = unquote(params[2])
+        if len(params) > 3: msnContact.caps = int(params[3])
+        if len(params) > 4: self.handleAvatarHelper(msnContact, params[4])
         self.contactStatusChanged(params[0], params[1], unquote(params[2]))
 
     def handle_FLN(self, params):
@@ -881,10 +1004,31 @@ class NotificationClient(MSNEventBase):
         # 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]))
+        userHandle = ""
+        screenName = ""
+        userGuid = ""
+       lists = -1
+        groups = []
+        for p in params:
+            if p[0] == 'N':
+                userHandle = getVal(p)
+            elif p[0] == 'F':
+                screenName = getVal(p)
+            elif p[0] == 'C':
+                userGuid = getVal(p)
+            elif p.isdigit():
+                lists = int(p)
+            else: # Must be the groups
+                try:
+                    groups = p.split(',')
+                except:
+                    raise MSNProtocolError, "Unknown LST " + str(params) # debug
+
+        if not userHandle or lists < 1:
+            raise MSNProtocolError, "Unknown LST " + str(params) # debug
+        contact = MSNContact(userGuid, userHandle, screenName, lists)
         if contact.lists & FORWARD_LIST:
-            contact.groups.extend(map(int, params[3].split(',')))
+            contact.groups.extend(map(str, groups))
         self._getStateData('list').addContact(contact)
         self._setStateData('last_contact', contact)
         sofar = self._getStateData('lst_sofar') + 1
@@ -912,7 +1056,7 @@ class NotificationClient(MSNEventBase):
             self._getStateData('list').privacy = listCodeToID[params[0].lower()]
         else:
             id = int(params[0])
-            self._fireCallback(id, int(params[1]), listCodeToID[params[2].lower()])
+            self._fireCallback(id, listCodeToID[params[1].lower()])
 
     def handle_GTC(self, params):
         # check to see if this is in response to a SYN
@@ -928,21 +1072,20 @@ class NotificationClient(MSNEventBase):
 
     def handle_SYN(self, params):
         id = int(params[0])
-        if len(params) == 2:
+        self._setStateData('phone', []) # Always needs to be set
+        if params[3] == 0: # No LST will be received. New account?
             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_reply', int(params[3]))
+            self._setStateData('lsg_reply', int(params[4]))
             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])
+            self._getStateData('list').groups[params[1]] = unquote(params[0])
 
         # Please see the comment above the requestListGroups / requestList methods
         # regarding support for this
@@ -954,7 +1097,9 @@ class NotificationClient(MSNEventBase):
         #        self._remStateData('groups')
 
     def handle_PRP(self, params):
-        if self._getState() == 'SYNC':
+        if params[1] == "MFN":
+            self._fireCallback(int(params[0]), unquote(params[2]))
+        elif self._getState() == 'SYNC':
             self._getStateData('phone').append((params[0], unquote(params[1])))
         else:
             self._fireCallback(int(params[0]), int(params[1]), unquote(params[3]))
@@ -964,7 +1109,7 @@ class NotificationClient(MSNEventBase):
         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]))
+            self.gotPhoneNumber(params[1], params[2], unquote(params[3]))
 
     def handle_ADG(self, params):
         checkParamLen(len(params), 5, 'ADG')
@@ -984,20 +1129,21 @@ class NotificationClient(MSNEventBase):
         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):
+    def handle_ADC(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
+        if numParams < 4 or params[1].upper() not in ('AL','BL','RL','FL', 'PL'):
+            raise MSNProtocolError, "Invalid Paramaters for ADC" # 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)
+        userHandle = getVal(params[2])
+        screenName = unquote(getVal(params[3]))
+        userGuid = None
+        if len(params) >= 5: userGuid = getVal(params[4])
+#        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], userGuid, userHandle, screenName):
+            self.userAddedMe(userGuid, userHandle, screenName)
 
     def handle_REM(self, params):
         numParams = len(params)
@@ -1005,19 +1151,13 @@ class NotificationClient(MSNEventBase):
             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]))
+        if not self._fireCallback(id, listCodeToID[listType], userHandle, groupID):
+            if listType.upper() == "RL": self.userRemovedMe(userHandle)
 
     def handle_XFR(self, params):
         checkParamLen(len(params), 5, 'XFR')
@@ -1049,9 +1189,22 @@ class NotificationClient(MSNEventBase):
         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.currentMessage = MSNMessage(length=messageLen, userHandle="NOTIFICATION", specialMessage=True)
         self.setRawMode()
 
+    def handle_UBX(self, params):
+        checkParamLen(len(params), 2, 'UBX')
+        try:
+            messageLen = int(params[1])
+        except ValueError: raise MSNProtocolError, "Invalid Parameter for UBX length argument"
+        self.currentMessage = MSNMessage(length=messageLen, userHandle=params[0], screenName="UBX", specialMessage=True)
+        self.setRawMode()
+
+    def handle_UUX(self, params):
+        checkParamLen(len(params), 2, 'UUX')
+        if params[1] != '0': return
+        id = int(params[0])
+        self._fireCallback(id)
 
     def handle_OUT(self, params):
         checkParamLen(len(params), 1, 'OUT')
@@ -1076,7 +1229,7 @@ class NotificationClient(MSNEventBase):
         self.pingCheckTask = task.LoopingCall(self.pingChecker)
         self.pingCheckTask.start(50.0)
 
-    def loggedIn(self, userHandle, screenName, verified):
+    def loggedIn(self, userHandle, verified):
         """
         Called when the client has logged in.
         The default behaviour of this method is to
@@ -1086,15 +1239,12 @@ class NotificationClient(MSNEventBase):
         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)
+        #self.factory.screenName = screenName
+        d = self.syncList()
         d.addCallback(self.listSynchronized)
         d.addCallback(self.pingCheckerStart)
 
@@ -1125,33 +1275,23 @@ class NotificationClient(MSNEventBase):
         """
         pass
 
-    def statusChanged(self, statusCode):
+    def avatarHashChanged(self, userHandle, hash):
         """
-        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.
+        Called when we receive the first, or a new <msnobj/> from a
+        contact.
 
-        @param statusCode: 3-letter status code
+        @param userHandle: contact who's msnobj has been changed
+        @param hash: sha1 hash of their avatar
         """
-        self.factory.status = statusCode
 
-    def gotContactStatus(self, statusCode, userHandle, screenName):
+    def statusChanged(self, statusCode):
         """
-        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.
+        Called when our status changes and it isn't in response to
+        a client command.
 
         @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
+        pass
 
     def contactStatusChanged(self, statusCode, userHandle, screenName):
         """
@@ -1163,12 +1303,12 @@ class NotificationClient(MSNEventBase):
         @param userHandle: the contact's user handle (passport)
         @param screenName: the contact's screen name
         """
+        pass
+
+    def contactPersonalChanged(self, userHandle, personal):
         msnContact = self.factory.contacts.getContact(userHandle)
-        if(not msnContact):
-            msnContact = MSNContact()
-            self.factory.contacts.addContact(msnContact)
-        msnContact.status = statusCode
-        msnContact.screenName = screenName
+        if(not msnContact): return
+        msnContact.personal = personal
 
     def contactOffline(self, userHandle):
         """
@@ -1182,7 +1322,19 @@ class NotificationClient(MSNEventBase):
         if(msnContact):
             msnContact.status = STATUS_OFFLINE
 
-    def gotPhoneNumber(self, listVersion, userHandle, phoneType, number):
+    def gotMessage(self, message):
+        pass
+
+    def msnAlertReceived(self, body, action, subscr):
+        pass
+
+    def initialEmailNotification(self, inboxunread, foldersunread):
+        pass
+
+    def realtimeEmailNotification(self, mailfrom, fromaddr, subject):
+        pass
+
+    def gotPhoneNumber(self, userHandle, phoneType, number):
         """
         Called when the server sends us phone details about
         a specific user (for example after a user is added
@@ -1191,17 +1343,15 @@ class NotificationClient(MSNEventBase):
         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):
+    def userAddedMe(self, userGuid, userHandle, screenName):
         """
         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
@@ -1211,18 +1361,15 @@ class NotificationClient(MSNEventBase):
 
         @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)
+            c = MSNContact(userGuid=userGuid, userHandle=userHandle, screenName=screenName)
             self.factory.contacts.addContact(c)
-        c.addToList(REVERSE_LIST)
+        c.addToList(PENDING_LIST)
 
-    def userRemovedMe(self, userHandle, listVersion):
+    def userRemovedMe(self, userHandle):
         """
         Called when a user removes us from their contact list
         (they are no longer on our reverseContacts list.
@@ -1233,10 +1380,8 @@ class NotificationClient(MSNEventBase):
         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)
@@ -1290,7 +1435,7 @@ class NotificationClient(MSNEventBase):
         """
         
         id, d = self._createIDMapping()
-        self.sendLine("CHG %s %s" % (id, status))
+        self.sendLine("CHG %s %s %s %s" % (id, status, str(MSNContact.MSNC1 | MSNContact.MSNC2 | MSNContact.MSNC3 | MSNContact.MSNC4), quote(self.msnobj.text)))
         def _cb(r):
             if self.factory: self.factory.status = r[0]
             return r
@@ -1352,7 +1497,7 @@ class NotificationClient(MSNEventBase):
         else: self.sendLine("BLP %s BL" % id)
         return d
 
-    def syncList(self, version):
+    def syncList(self):
         """
         Used for keeping an up-to-date contact list.
         A callback is added to the returned Deferred
@@ -1367,8 +1512,6 @@ class NotificationClient(MSNEventBase):
         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
@@ -1379,9 +1522,9 @@ class NotificationClient(MSNEventBase):
         """
 
         self._setState('SYNC')
-        id, d = self._createIDMapping(data=str(version))
+        id, d = self._createIDMapping(data=None)
         self._setStateData('synid',id)
-        self.sendLine("SYN %s %s" % (id, version))
+        self.sendLine("SYN %s %s %s" % (id, 0, 0))
         def _cb(r):
             self.changeStatus(STATUS_ONLINE)
             if r[0] is not None:
@@ -1461,8 +1604,9 @@ class NotificationClient(MSNEventBase):
         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])
+            if self.factory.contacts:
+                self.factory.contacts.version = r[0]
+                self.factory.contacts.setGroup(r[1], r[2])
             return r
         return d.addCallback(_cb)
 
@@ -1515,7 +1659,7 @@ class NotificationClient(MSNEventBase):
             return r
         return d.addCallback(_cb)
 
-    def addContact(self, listType, userHandle, groupID=0):
+    def addContact(self, listType, userHandle, groupID=""):
         """
         Used to add a contact to the desired list.
         A default callback is added to the returned
@@ -1544,40 +1688,35 @@ class NotificationClient(MSNEventBase):
         """
         
         id, d = self._createIDMapping()
+        try: # Make sure the contact isn't actually on the list
+            if(self.factory.contacts.getContact(userHandle).lists & listType): return
+        except AttributeError: pass
         listType = listIDToCode[listType].upper()
         if listType == "FL":
-            self.sendLine("ADD %s FL %s %s %s" % (id, userHandle, userHandle, groupID))
+            self.sendLine("ADC %s %s N=%s F=%s %s" % (id, listType, userHandle, userHandle, groupID))
         else:
-            self.sendLine("ADD %s %s %s %s" % (id, listType, userHandle, userHandle))
+            self.sendLine("ADC %s %s N=%s" % (id, listType, userHandle))
 
         def _cb(r):
-            self.factory.contacts.version = r[2]
-            c = self.factory.contacts.getContact(r[1])
+            if not self.factory: return
+            c = self.factory.contacts.getContact(r[2])
             if not c:
-                c = MSNContact(userHandle=r[1])
-            if r[3]: c.groups.append(r[3])
+                c = MSNContact(userGuid=r[1], userHandle=r[2], screenName=r[3])
+            #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):
+    def remContact(self, listType, userHandle):
         """
         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.
+        to reflect the new contact information.
 
         @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.
@@ -1588,9 +1727,16 @@ class NotificationClient(MSNEventBase):
         """
         
         id, d = self._createIDMapping()
+        try: # Make sure the contact is actually on this list
+            if(not (self.factory.contacts.getContact(userHandle).lists & listType)): return
+        except AttributeError: return
         listType = listIDToCode[listType].upper()
         if listType == "FL":
-            self.sendLine("REM %s FL %s %s" % (id, userHandle, groupID))
+            try:
+                c = self.factory.contacts.getContact(userHandle)
+                userGuid = c.userGuid
+            except AttributeError: return
+            self.sendLine("REM %s FL %s" % (id, userGuid))
         else:
             self.sendLine("REM %s %s %s" % (id, listType, userHandle))
 
@@ -1627,13 +1773,32 @@ class NotificationClient(MSNEventBase):
         """
 
         id, d = self._createIDMapping()
-        self.sendLine("REA %s %s %s" % (id, self.factory.userHandle, quote(newName)))
+        self.sendLine("PRP %s MFN %s" % (id, quote(newName)))
         def _cb(r):
-            if(self.factory.contacts): self.factory.contacts.version = r[0]
-            self.factory.screenName = r[1]
+            self.factory.screenName = r[0]
             return r
         return d.addCallback(_cb)
 
+    def changePersonalMessage(self, personal):
+        id, d = self._createIDMapping()
+        data = ""
+        if(personal):
+            data = "<Data><PSM>" + personal + "</PSM><CurrentMedia></CurrentMedia></Data>"
+        self.sendLine("UUX %s %s" % (id, len(data)))
+        self.transport.write(data)
+        def _cb(r):
+            self.factory.personal = personal
+        return d.addCallback(_cb)
+
+    def changeAvatar(self, imageData, push):
+        if(self.msnobj and imageData == self.msnobj.imageData): return
+        if(imageData):
+            self.msnobj.setData(self.factory.userHandle, imageData)
+        else:
+            self.msnobj.setNull()
+        if(push): self.changeStatus(self.factory.status) # Push to server
+
+
     def requestSwitchboardServer(self):
         """
         Used to request a switchboard server to use for conversations.
@@ -1693,7 +1858,6 @@ class NotificationFactory(ClientFactory):
     passportServer = 'https://nexus.passport.com/rdr/pprdr.asp'
     status = 'FLN'
     protocol = NotificationClient
-    initialListVersion = 0
 
 
 # XXX: A lot of the state currently kept in
@@ -1730,10 +1894,12 @@ class SwitchboardClient(MSNEventBase):
 
     _iCookie = 0
 
-    def __init__(self):
+    def __init__(self, msnobj=None):
         MSNEventBase.__init__(self)
         self.pendingUsers = {}
         self.cookies = {'iCookies' : {}, 'external' : {}} # will maybe be moved to a factory in the future
+        self.p2pHandlers = []
+        self.msnobj = msnobj
 
     def connectionMade(self):
         MSNEventBase.connectionMade(self)
@@ -1768,7 +1934,7 @@ class SwitchboardClient(MSNEventBase):
 
     def _checkFileInvitation(self, message, info):
         """ helper method for checkMessage """
-        if not info.get('Application-Name', '').lower() == 'file transfer': return 0
+        if not info.get('Application-GUID', '').lower() == '{5D3E02AB-6190-11d3-BBBB-00C04F795683}': return 0
         try:
             cookie = info['Invitation-Cookie']
             fileName = info['Application-File']
@@ -1808,6 +1974,34 @@ class SwitchboardClient(MSNEventBase):
         del self.cookies['external'][iCookie]
         return 1
 
+    def _checkP2PMessage(self, message, ctypes):
+        """ helper method """
+        if "application/x-msnmsgrp2p" in ctypes:
+            if(self.msnobj.text and message.message.find("INVITE") > 0): # Probably a new one
+                handler = msnp2p.MSNP2P_Avatar_Send(to=message.userHandle, fro=self.userHandle, msnobj=self.msnobj)
+                self.p2pHandlers.append(handler)
+
+            for handler in self.p2pHandlers:
+                if handler.to != message.userHandle: continue
+                handler.processPacket(message.message)
+                packet = handler.getNextPacket()
+                while(packet):
+                    msnmessage = MSNMessage(message=packet)
+                    msnmessage.setHeader("Content-Type", "application/x-msnmsgrp2p")
+                    msnmessage.setHeader("P2P-Dest", handler.to)
+                    msnmessage.ack = MSNMessage.MESSAGE_ACK_FAT
+                    self.sendMessage(msnmessage)
+                    packet = handler.getNextPacket()
+                image = handler.getImage()
+                if(image): # We've got the avatar!
+                    self.gotAvatarImage(handler.to, image)
+                if(handler.isFinished()): # Time to end the P2P session
+                    self.p2pHandlers.remove(handler)
+                    break
+            return 1
+        else:
+            return 0
+
     def checkMessage(self, message):
         """
         hook for detecting any notification type messages
@@ -1824,6 +2018,7 @@ class SwitchboardClient(MSNEventBase):
                     info[key] = val.lstrip()
                 except ValueError: continue
             if self._checkFileInvitation(message, info) or self._checkFileInfo(message, info) or self._checkFileResponse(message, info): return 0
+        if self._checkP2PMessage(message, cTypes): return 0
         return 1
 
     # negotiation
@@ -1913,6 +2108,15 @@ class SwitchboardClient(MSNEventBase):
         """
         pass
 
+    def gotAvatarImage(self, userHandle, image):
+        """
+        called when we receive an avatar from a user
+
+        @param userHandle: the person who's avatar we have got
+        @param image: the avatar image
+        """
+        pass
+
     def userTyping(self, message):
         """
         called when we receive the special type of message notifying
@@ -1971,7 +2175,7 @@ class SwitchboardClient(MSNEventBase):
                  the return value is None.
         """
 
-        if message.ack not in ('A','N'): id, d = self._nextTransactionID(), None
+        if message.ack not in ('A','N','D'): 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))
@@ -1985,6 +2189,16 @@ class SwitchboardClient(MSNEventBase):
         self.transport.write(message.message)
         return d
 
+    def sendAvatarRequest(self, userHandle, msnobj):
+        handler = msnp2p.MSNP2P_Avatar_Receive(to=userHandle, fro=self.userHandle, msnobj=msnobj)
+       self.p2pHandlers.append(handler)
+        msnmessage = MSNMessage(message=handler.getNextPacket())
+        msnmessage.setHeader("Content-Type", "application/x-msnmsgrp2p")
+        msnmessage.setHeader("P2P-Dest", handler.to)
+        msnmessage.ack = MSNMessage.MESSAGE_ACK_FAT
+        self.sendMessage(msnmessage)
+    
+
     def sendTypingNotification(self):
         """
         used to send a typing notification. Upon receiving this
@@ -2162,7 +2376,7 @@ class FileReceive(LineReceiver):
         return factor * 256 + extra
 
     def lineReceived(self, line):
-        temp = line.split()
+        temp = line.split(' ')
         if len(temp) == 1: params = []
         else: params = temp[1:]
         cmd = temp[0]
@@ -2270,7 +2484,7 @@ class FileSend(LineReceiver):
         self.file.close()
 
     def lineReceived(self, line):
-        temp = line.split()
+        temp = line.split(' ')
         if len(temp) == 1: params = []
         else: params = temp[1:]
         cmd = temp[0]
@@ -2358,6 +2572,9 @@ errorCodes = {
     301 : "Too many FND responses",
     302 : "Not logged in",
 
+    402 : "Error accessing contact list",
+    403 : "Error accessing contact list",
+
     500 : "Internal server error",
     501 : "Database server error",
     502 : "Command disabled",
@@ -2423,7 +2640,8 @@ listIDToCode = {
     FORWARD_LIST : 'fl',
     BLOCK_LIST   : 'bl',
     ALLOW_LIST   : 'al',
-    REVERSE_LIST : 'rl'
+    REVERSE_LIST : 'rl',
+    PENDING_LIST : 'pl'
 
 }
 
index fc29c99f4e99552bf5bbef8b9391020542a535c4..4f768e38830ab6fd8b303d324ebbb734c219ea3d 100644 (file)
@@ -1,34 +1,6 @@
-# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Copyright 2004-2005 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):
@@ -38,6 +10,42 @@ def getLang(el):
        return el.getAttribute((u'http://www.w3.org/XML/1998/namespace', u'lang'))
 
 
+import random
+def random_guid():
+       format = "{%4X%4X-%4X-%4X-%4X-%4X%4X%4X}"
+       data = []
+       for x in xrange(8):
+               data.append(random.random() * 0xAAFF + 0x1111)
+       data = tuple(data)
+       
+       return format % data
+
+
+import base64
+def b64enc(s):
+       return base64.encodestring(s).replace('\n', '')
+
+def b64dec(s):
+       return base64.decodestring(s)
+
+try:
+       import Image
+       import StringIO
+       
+       def convertToPNG(imageData):
+               inbuff = StringIO.StringIO(imageData)
+               outbuff = StringIO.StringIO()
+               Image.open(inbuff).save(outbuff, "PNG")
+               outbuff.seek(0)
+               imageData = outbuff.read()
+               return imageData
+except ImportError:
+       print "WARNING! Only PNG avatars will be understood by this transport. Please install the Python Imaging Library."
+
+       def convertToPNG(imageData):
+               return ""
+
+
 errorCodeMap = {
 "bad-request"                  :       400,
 "conflict"                     :       409,
@@ -63,60 +71,23 @@ errorCodeMap = {
 "unexpected-request"           :       400
 }
 
-def doPath(path):
-       if(path and path[0] == "/"):
-               return path
-       else:
-               return "../" + path
 
-
-def parseText(text):
-       t = TextParser()
+def parseText(text, beExtremelyLenient=False):
+       t = TextParser(beExtremelyLenient)
        t.parseString(text)
        return t.root
 
-def parseFile(filename):
-       t = TextParser()
+def parseFile(filename, beExtremelyLenient=False):
+       t = TextParser(beExtremelyLenient)
        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):
+       if checkTwistedCached == None:
                import twisted.copyright
                checkTwistedCached = (VersionNumber(twisted.copyright.version) >= VersionNumber("2.0.0"))
        return checkTwistedCached
@@ -127,11 +98,11 @@ class VersionNumber:
                index = 0 
                flag = True
                for c in vstring:
-                       if(c == '.'):
+                       if c == '.':
                                self.varray.append(0)
                                index += 1
                                flag = True
-                       elif(c.isdigit() and flag):
+                       elif c.isdigit() and flag:
                                self.varray[index] *= 10
                                self.varray[index] += int(c)
                        else:
@@ -140,25 +111,58 @@ class VersionNumber:
        def __cmp__(self, other):
                i = 0
                while(True):
-                       if(i == len(other.varray)):
-                               if(i < len(self.varray)):
+                       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)):
+                       if i == len(self.varray):
+                               if i < len(other.varray):
                                        return -1
                                else:
                                        return 0
 
-                       if(self.varray[i] > other.varray[i]):
+                       if self.varray[i] > other.varray[i]:
                                return 1
-                       elif(self.varray[i] < other.varray[i]):
+                       elif self.varray[i] < other.varray[i]:
                                return -1
 
                        i += 1
        
 
+if checkTwisted():
+       from twisted.xish.domish import SuxElementStream
+else:
+       from tlib.domish import SuxElementStream
+class TextParser:
+       """ Taken from http://xoomer.virgilio.it/dialtone/rsschannel.py """
+
+       def __init__(self, beExtremelyLenient=False):
+               self.root = None
+               self.beExtremelyLenient = beExtremelyLenient
+
+       def parseFile(self, filename):
+               return self.parseString(file(filename).read())
+
+       def parseString(self, data):
+               es = SuxElementStream()
+               es.beExtremelyLenient = self.beExtremelyLenient
+               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 RollingStack:
        def __init__(self, size):
@@ -167,7 +171,7 @@ class RollingStack:
        
        def push(self, data):
                self.lst.append(str(data))
-               if(len(self.lst) > self.size):
+               if len(self.lst) > self.size:
                        self.lst.remove(self.lst[0])
        
        def grabAll(self):
index 98cdfadcc1688596cdac593023a13c2d9c1fcdc1..83d4f748a0ec0d24b2225c0a19487dd2ebe34c3f 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Copyright 2004-2005 James Bunton <james@delx.cjb.net>
 # Licensed for distribution under the GPL version 2, check COPYING for details
 
 import utils
@@ -6,48 +6,77 @@ if(utils.checkTwisted()):
        from twisted.xish.domish import Element
 else:
        from tlib.domish import Element
-
+from debug import LogEvent, INFO, WARN
 import os
 import os.path
-import debug
 import config
 import legacy
 
-SPOOL_UMASK = 0177
+SPOOL_UMASK = 0077
+
+
+def unmangle(file):
+       chunks = file.split("%")
+       end = chunks.pop()
+       file = "%s@%s" % ("%".join(chunks), end)
+       return file
+
+def mangle(jid):
+       return jid.replace("@", "%")
+
 
 class XDB:
        """
-       Class for storage of data. Compatible with xdb_file from Jabberd1.4.x
-       Allows PyMSN-t to be compatible with MSN-t
+       Class for storage of data.
        
        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) :
+               self.name = os.path.abspath(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('@', '%')
+                       file = mangle(file)
                
-               document = utils.parseFile(self.name + "/" + file + ".xml")
+               hash = file[0:2]
+               document = utils.parseFile(self.name + "/" + hash + "/" + file + ".xml")
                
                return document
        
        def __writeFile(self, file, text):
                if(self.mangle):
-                       file = file.replace('@', '%')
+                       file = mangle(file)
                
                prev_umask = os.umask(SPOOL_UMASK)
-               f = open(self.name + "/" + file + ".xml", "w")
+               hash = file[0:2]
+               pre = self.name + "/" + hash + "/"
+               if not os.path.exists(pre):
+                       os.makedirs(pre)
+               f = open(pre + file + ".xml", "w")
                f.write(text)
                f.close()
                os.umask(prev_umask)
        
+       def files(self):
+               """ Returns a list containing the files in the current XDB database """
+               files = []
+               for dir in os.listdir(self.name):
+                       if(os.path.isdir(self.name + "/" + dir)):
+                               files.extend(os.listdir(self.name + "/" + dir))
+               if self.mangle:
+                       files = [unmangle(x)[:-4] for x in files]
+               else:
+                       files = [x[:-4] for x in files]
+
+               while files.count(''):
+                       files.remove('')
+
+               return files
        
        def request(self, file, xdbns):
                """ Requests a specific xdb namespace from the XDB 'file' """
@@ -79,18 +108,20 @@ class XDB:
                        document.addChild(element)
                        
                        self.__writeFile(file, document.toXml())
-               except:
-                       debug.log("XDB error writing entry %s to file %s" % (xdbns, file))
+               except IOError, e:
+                       LogEvent(WARN, "", "IOError " + str(e))
                        raise
        
        def remove(self, file):
                """ Removes an XDB file """
-               file = self.name + "/" + file + ".xml"
+               file = self.name + "/" + file[0:2] + "/" + file + ".xml"
                if(self.mangle):
-                       file = file.replace('@', '%')
+                       file = mangle(file)
                try:
                        os.remove(file)
-               except:
-                       debug.log("XDB error removing file " + file)
+               except IOError, e:
+                       LogEvent(WARN, "", "IOError " + str(e))
                        raise
+       
+
 
index b630d95ebd93f80128912c564fafeca0ce5911b6..e461d7ef3fd0fcc4862e587d3fa7f109d95c0b78 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright 2004 James Bunton <james@delx.cjb.net>
+# Copyright 2004-2005 James Bunton <james@delx.cjb.net>
 # Licensed for distribution under the GPL version 2, check COPYING for details
 
 
@@ -9,6 +9,7 @@ import utils
 import config
 
 
+
 def invalidError(text):
        print text
        print "Exiting..."
@@ -17,32 +18,45 @@ def invalidError(text):
 
 def reloadConfig():
        # Find out where the config file is
-       configFile = "../config.xml"
-       if(len(sys.argv) == 2):
+       configFile = "config.xml"
+       if len(sys.argv) == 2:
                configFile = sys.argv[1]
 
        # Check the file exists
-       if(not os.path.isfile(configFile)):
+       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)
+       try:
+               root = utils.parseFile(configFile)
+       except Exception, e:
+               invalidError("Error parsing configuration file: " + str(e))
 
        # Store all the options in config
        for el in root.elements():
                try:
                        tag = el.name
                        cdata = str(el)
-                       if(cdata):
+                       children = [x for x in el.elements()]
+                       if children:
+                               # For options like <admins><jid>user1@host.com</jid><jid>user2@host.com</jid></admins>
+                               if type(getattr(config, tag)) != list:
+                                       invalidError("Tag %s in your configuration file should be a list (ie, must have subtags)." % (tag))
+                               myList = getattr(config, tag)
+                               for child in children:
+                                       s = child.__str__()
+                                       myList.append(s)
+                       elif 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))
+                               if type(getattr(config, tag)) != str:
+                                       invalidError("Tag %s in your configuration file should not be a string (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))
+                               t = type(getattr(config, tag))
+                               if not (t == bool or t == int):
+                                       invalidError("Tag %s in your configuration file should not be a boolean (ie, must have cdata or subtags)." % (tag))
                                setattr(config, tag, True)
                except AttributeError:
                        print "Tag %s in your configuration file is not a defined tag. Ignoring!" % (tag)