From 85e6c0f55bd0d46cad7f94d15b52c7b64dc94c08 Mon Sep 17 00:00:00 2001 From: jamesbunton Date: Mon, 31 Oct 2005 01:09:16 +0000 Subject: [PATCH] Reimport and tags (0.10.1) git-svn-id: http://delx.cjb.net/svn/pymsnt/trunk@3 55fbd22a-6204-0410-b2f0-b6c764c7e90a committer: jamesbunton --- PyMSNt | 7 +- PyMSNt.tac | 25 ++ README | 19 +- TODO | 42 +- config-example.xml | 45 +-- src/avatar.py | 123 ++++++ src/baseproto/__init__.py | 4 +- src/baseproto/glue.py | 83 +++- src/config.py | 15 +- src/contact.py | 225 +++++++++++ src/debug.py | 103 ++--- src/disco.py | 213 ++++++---- src/groupchat.py | 30 +- src/housekeep.py | 117 ++++++ src/jabw.py | 156 +++++--- src/lang.py | 176 ++++++++- src/legacy/__init__.py | 4 +- src/legacy/defaultAvatar.png | Bin 0 -> 14982 bytes src/legacy/defaultJabberAvatar.png | Bin 0 -> 7363 bytes src/legacy/glue.py | 339 ++++++++-------- src/legacy/legacylist.py | 187 +++++++++ src/legacy/msnw.py | 604 ++++++++++++++++------------- src/main.py | 264 +++++-------- src/misciq.py | 391 +++++++++++++++++-- src/register.py | 98 +++-- src/session.py | 218 +++++++---- src/tlib/msn.py | 530 +++++++++++++++++-------- src/utils.py | 164 ++++---- src/xdb.py | 67 +++- src/xmlconfig.py | 34 +- 30 files changed, 2962 insertions(+), 1321 deletions(-) create mode 100644 PyMSNt.tac create mode 100644 src/avatar.py create mode 100644 src/contact.py create mode 100644 src/housekeep.py create mode 100644 src/legacy/defaultAvatar.png create mode 100644 src/legacy/defaultJabberAvatar.png create mode 100644 src/legacy/legacylist.py diff --git a/PyMSNt b/PyMSNt index a33c171..d73cc9d 100755 --- 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 index 0000000..e41586a --- /dev/null +++ b/PyMSNt.tac @@ -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 af64581..2f767b5 100644 --- 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 eb1ba77..20a1509 100644 --- 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) diff --git a/config-example.xml b/config-example.xml index c495fd8..31b7b5f 100644 --- a/config-example.xml +++ b/config-example.xml @@ -4,15 +4,14 @@ msn + + - -PyMSNt.pid - 127.0.0.1 @@ -33,42 +32,20 @@ Do not include the jid of the transport --> - - - - - - - - - - - - - - - - - - - - - - - - - - -debug.log - + + + + + + + diff --git a/src/avatar.py b/src/avatar.py new file mode 100644 index 0000000..3912450 --- /dev/null +++ b/src/avatar.py @@ -0,0 +1,123 @@ +# Copyright 2005 James Bunton +# 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.") + + + diff --git a/src/baseproto/__init__.py b/src/baseproto/__init__.py index df8b9dd..9b9dd6a 100644 --- a/src/baseproto/__init__.py +++ b/src/baseproto/__init__.py @@ -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 diff --git a/src/baseproto/glue.py b/src/baseproto/glue.py index d273dfc..062ce51 100644 --- a/src/baseproto/glue.py +++ b/src/baseproto/glue.py @@ -1,12 +1,24 @@ -# Copyright 2004 James Bunton +# Copyright 2005 James Bunton # 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): diff --git a/src/config.py b/src/config.py index 9090172..61d4e71 100644 --- a/src/config.py +++ b/src/config.py @@ -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 index 0000000..48cb184 --- /dev/null +++ b/src/contact.py @@ -0,0 +1,225 @@ +# Copyright 2005 James Bunton +# 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) + + diff --git a/src/debug.py b/src/debug.py index d284ee9..780f27a 100644 --- a/src/debug.py +++ b/src/debug.py @@ -1,76 +1,37 @@ -# Copyright 2004 James Bunton +# Copyright 2004-2005 James Bunton # 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) diff --git a/src/disco.py b/src/disco.py index 000677e..a498fc7 100644 --- a/src/disco.py +++ b/src/disco.py @@ -1,33 +1,63 @@ -# Copyright 2004 James Bunton +# Copyright 2004-2005 James Bunton # 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) + diff --git a/src/groupchat.py b/src/groupchat.py index 2eeef61..27b4d0e 100644 --- a/src/groupchat.py +++ b/src/groupchat.py @@ -1,4 +1,4 @@ -# Copyright 2004 James Bunton +# Copyright 2004-2005 James Bunton # 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 index 0000000..6a472ca --- /dev/null +++ b/src/housekeep.py @@ -0,0 +1,117 @@ +# Copyright 2005 James Bunton +# 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] + diff --git a/src/jabw.py b/src/jabw.py index fa7f4f4..0d0a1f5 100644 --- a/src/jabw.py +++ b/src/jabw.py @@ -1,4 +1,4 @@ -# Copyright 2004 James Bunton +# Copyright 2004-2005 James Bunton # 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 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 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 diff --git a/src/lang.py b/src/lang.py index ecc7327..0e616e4 100644 --- a/src/lang.py +++ b/src/lang.py @@ -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 # 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 + 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 , Sander Devrieze 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 # Former translator: Alexandre Viard @@ -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 + 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 diff --git a/src/legacy/__init__.py b/src/legacy/__init__.py index 0df7da3..6e5a50a 100644 --- a/src/legacy/__init__.py +++ b/src/legacy/__init__.py @@ -1,6 +1,6 @@ # Copyright 2004 James Bunton # 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 index 0000000000000000000000000000000000000000..5d37a21301b9222a06e3808204bbaa2af1de5bb1 GIT binary patch literal 14982 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 literal 0 HcmV?d00001 diff --git a/src/legacy/defaultJabberAvatar.png b/src/legacy/defaultJabberAvatar.png new file mode 100644 index 0000000000000000000000000000000000000000..0427e1f922605c15bfe17a1cfb8443c1a306d2d3 GIT binary patch literal 7363 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 literal 0 HcmV?d00001 diff --git a/src/legacy/glue.py b/src/legacy/glue.py index 1747492..a809f8e 100644 --- a/src/legacy/glue.py +++ b/src/legacy/glue.py @@ -1,28 +1,36 @@ -# Copyright 2004 James Bunton +# Copyright 2004-2005 James Bunton # 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 index 0000000..3e8c8a0 --- /dev/null +++ b/src/legacy/legacylist.py @@ -0,0 +1,187 @@ +# Copyright 2004-2005 James Bunton +# 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 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.") + + diff --git a/src/legacy/msnw.py b/src/legacy/msnw.py index 85b9256..0644246 100644 --- a/src/legacy/msnw.py +++ b/src/legacy/msnw.py @@ -1,14 +1,20 @@ -# Copyright 2004 James Bunton +# Copyright 2004-2005 James Bunton # 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¬ification_id=%s&message_id=%s&agent=messenger" % (action, notid, msgid) - subscrurl = "%s¬ification_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) diff --git a/src/main.py b/src/main.py index 7010850..70d0733 100644 --- a/src/main.py +++ b/src/main.py @@ -1,53 +1,40 @@ -# Copyright 2004 James Bunton +# Copyright 2004-2005 James Bunton # 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() - diff --git a/src/misciq.py b/src/misciq.py index bbbde0f..56b5be6 100644 --- a/src/misciq.py +++ b/src/misciq.py @@ -1,4 +1,4 @@ -# Copyright 2004 James Bunton +# Copyright 2004-2005 James Bunton # 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) diff --git a/src/register.py b/src/register.py index 880aef2..dffca0c 100644 --- a/src/register.py +++ b/src/register.py @@ -1,4 +1,4 @@ -# Copyright 2004 James Bunton +# Copyright 2004-2005 James Bunton # 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): diff --git a/src/session.py b/src/session.py index 59b672a..28b7322 100644 --- a/src/session.py +++ b/src/session.py @@ -1,12 +1,18 @@ -# Copyright 2004 James Bunton +# Copyright 2004-2005 James Bunton # 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 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.") + diff --git a/src/tlib/msn.py b/src/tlib/msn.py index 9dc1e50..c5c9d39 100644 --- a/src/tlib/msn.py +++ b/src/tlib/msn.py @@ -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 @@ -16,11 +17,11 @@ # """ -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¬ification_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¬ification_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("") + 5 + p2 = lm.find("") + 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 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 = "" + personal + "" + 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' } diff --git a/src/utils.py b/src/utils.py index fc29c99..4f768e3 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,34 +1,6 @@ -# Copyright 2004 James Bunton +# Copyright 2004-2005 James Bunton # 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): diff --git a/src/xdb.py b/src/xdb.py index 98cdfad..83d4f74 100644 --- a/src/xdb.py +++ b/src/xdb.py @@ -1,4 +1,4 @@ -# Copyright 2004 James Bunton +# Copyright 2004-2005 James Bunton # 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 + + diff --git a/src/xmlconfig.py b/src/xmlconfig.py index b630d95..e461d7e 100644 --- a/src/xmlconfig.py +++ b/src/xmlconfig.py @@ -1,4 +1,4 @@ -# Copyright 2004 James Bunton +# Copyright 2004-2005 James Bunton # 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 user1@host.comuser2@host.com + 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 127.0.0.1 - 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 - 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) -- 2.39.2