# Copyright 2004-2006 Daniel Henninger # Licensed for distribution under the GPL version 2, check COPYING for details import config import utils import legacy import jabw import contact import avatar import globals from debug import LogEvent, INFO, WARN, ERROR import lang from twisted.words.protocols.jabber.jid import internJID def makeSession(pytrans, jabberID, ulang, rosterID): """ 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 """ LogEvent(INFO, jabberID) if pytrans.sessions.has_key(jabberID): LogEvent(INFO, jabberID, "Removing existing session") pytrans.sessions[jabberID].removeMe() result = pytrans.xdb.getRegistration(jabberID) if result: username, password = result return Session(pytrans, jabberID, username, password, ulang, rosterID) else: return None class Session(jabw.JabberConnection): """ A class to represent each registered user's session with the legacy network. Exists as long as there is a Jabber resource for the user available """ def __init__(self, pytrans, jabberID, username, password, ulang, rosterID): """ Initialises the session object and connects to the legacy network """ jabw.JabberConnection.__init__(self, pytrans, jabberID) LogEvent(INFO, jabberID) self.pytrans = pytrans self.alive = True self.ready = False # Only ready when we're logged into the legacy service self.jabberID = jabberID # the JabberID of the Session's user self.username = username # the legacy network ID of the Session's user self.password = password self.description = None self.nickname = None self.nicknamePri = 0 # priority of nickname: # 0 = no nickname # 1 = vcard based nickname # 2 = pep based nickname self.avatar = None self.avatarPri = 0 # priority of avatar: # 0 = no avatar # 1 = default avatar # 2 = vcard based avatar # 3 = iq based avatar # 4 = pep based avatar self.lang = ulang if rosterID.resource == "registered": self.registeredmunge = True else: self.registeredmunge = False self.show = None self.status = None self.url = None self.resourceList = {} self.groupchats = [] self.capabilities = [] self.contactList = contact.ContactList(self) self.legacycon = legacy.LegacyConnection(self.username, self.password, self) self.pytrans.legacycon = self.legacycon self.contactList.legacyList = self.legacycon.legacyList if config.sessionGreeting: self.sendMessage(to=self.jabberID, fro=config.jid, body=config.sessionGreeting) # Set default avatar to start with, should do this better self.legacycon.updateAvatar() self.avatarPri = 1 self.updateNickname(self.nickname) self.updateDescription(self.description) self.doVCardUpdate() if not config.disableIQAvatars: self.doIQAvatarUpdate() self.doDiscoRequest() LogEvent(INFO, self.jabberID, "Created!") self.pytrans.serviceplugins['Statistics'].stats["TotalSessions"] += 1 self.pytrans.serviceplugins['Statistics'].stats["OnlineSessions"] += 1 if len(self.pytrans.sessions)+1 > self.pytrans.serviceplugins['Statistics'].stats["MaxConcurrentSessions"]: self.pytrans.serviceplugins['Statistics'].stats["MaxConcurrentSessions"] = len(self.pytrans.sessions)+1 self.pytrans.serviceplugins['Statistics'].sessionUpdate(self.jabberID, "Connections", 1) def removeMe(self): """ Safely removes the session object, including sending messages for each legacy related item on the user's contact list """ # Send offline presence to Jabber ID # Delete all objects cleanly # Remove this Session object from the pytrans LogEvent(INFO, self.jabberID) # Mark as dead self.alive = False self.ready = False # Send offline presence to the user if self.pytrans: tmpjid = config.jid if self.registeredmunge: tmpjid = tmpjid + "/registered" self.sendPresence(to=self.jabberID, fro=tmpjid, ptype="unavailable") self.pytrans.serviceplugins['Statistics'].stats["OnlineSessions"] -= 1 # Remove any groupchats we may be in for groupchat in utils.copyList(self.groupchats): groupchat.removeMe() # Clean up stuff on the legacy service end (including sending offline presences for all contacts) if self.legacycon: self.legacycon.removeMe() self.legacycon = None if self.contactList: self.contactList.removeMe() self.contactList = None 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 LogEvent(INFO, self.jabberID, "Removed!") def hasCapability(self, capability): for c in self.capabilities: if c == capability: return True return False def doDiscoRequest(self): def userDiscoInfoReceived(el): if not self.alive: return LogEvent(INFO, self.jabberID) fro = el.getAttribute("from") for e in el.elements(): if e.name == "query" and e.uri == globals.DISCO_INFO: for item in e.elements(): if item.name == "feature": var = item.getAttribute("var") self.capabilities.append(var) LogEvent(INFO, self.jabberID, "Capabilities of %r:\n\t%s" % (fro, "\n\t".join(self.capabilities))) def serverDiscoInfoReceived(el): if not self.alive: return LogEvent(INFO, self.jabberID) fro = el.getAttribute("from") for e in el.elements(): if e.name == "query" and e.uri == globals.DISCO_INFO: for item in e.elements(): if item.name == "feature": var = item.getAttribute("var") self.capabilities.append(var) LogEvent(INFO, self.jabberID, "Capabilities of %r:\n\t%s" % (fro, "\n\t".join(self.capabilities))) def errback(args=None): LogEvent(INFO, self.jabberID, "Error fetching disco info") LogEvent(INFO, self.jabberID, "Fetching disco info from %r" % self.jabberID) d = self.sendDiscoRequest(to=self.jabberID, fro=config.jid) d.addCallback(userDiscoInfoReceived) d.addErrback(errback) sjid = internJID(self.jabberID).host LogEvent(INFO, self.jabberID, "Fetching disco info from %r" % sjid) d = self.sendDiscoRequest(to=sjid, fro=config.jid) d.addCallback(serverDiscoInfoReceived) d.addErrback(errback) 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.uri == globals.VCARD: vCard = e break if not vCard: return for e in vCard.elements(): if e.name == "DESC": self.updateDescription(e.__str__()) if e.name == "NICKNAME" and self.nicknamePri < 1: self.updateNickname(e.__str__()) self.nicknamePri = 1 if e.name == "PHOTO" and not config.disableAvatars and not config.disableVCardAvatars and self.avatarPri < 2: imageData = avatar.parsePhotoEl(e) if not imageData: errback(Exception("Invalid image data")) # Possibly it wasn't in a supported format? self.avatar = self.pytrans.avatarCache.setAvatar(imageData) self.legacycon.updateAvatar(self.avatar) self.avatarPri = 2 def errback(args=None): LogEvent(INFO, self.jabberID, "Error fetching vcard (avatar)") LogEvent(INFO, self.jabberID, "Fetching vcard") d = self.sendVCardRequest(to=self.jabberID, fro=config.jid) d.addCallback(vCardReceived) d.addErrback(errback) def doIQAvatarUpdate(self): if config.disableAvatars or config.disableIQAvatars: return def errback(args=None): LogEvent(INFO, self.jabberID, "Error fetching IQ-based avatar") def storageAvatarReceived(el): if not self.alive: return LogEvent(INFO, self.jabberID) qtype = el.getAttribute("type") if qtype == "error": return query = None for e in el.elements(): if e.name == "query" and e.uri == globals.STORAGEAVATAR: query = e break if not query: return for e in query.elements(): if e.name == "data" and self.avatarPri < 3: imageData = avatar.parseIQPhotoEl(e) if not imageData: errback(Exception("Invalid image data")) # Possibly it wasn't in a supported format? self.avatar = self.pytrans.avatarCache.setAvatar(imageData) self.legacycon.updateAvatar(self.avatar) self.avatarPri = 3 def iqAvatarReceived(el): if not self.alive: return LogEvent(INFO, self.jabberID) qtype = el.getAttribute("type") if qtype == "error": LogEvent(INFO, self.jabberID, "That didn't work, let's try an IQ-storage-based avatar") d = self.sendStorageAvatarRequest(to=self.jabberID, fro=config.jid) d.addCallback(storageAvatarReceived) d.addErrback(errback) query = None for e in el.elements(): if e.name == "query" and e.uri == globals.IQAVATAR: query = e break if not query: return for e in query.elements(): if e.name == "data" and self.avatarPri < 3: imageData = avatar.parseIQPhotoEl(e) if not imageData: errback(Exception("Invalid image data")) # Possibly it wasn't in a supported format? self.avatar = self.pytrans.avatarCache.setAvatar(imageData) self.legacycon.updateAvatar(self.avatar) self.avatarPri = 3 LogEvent(INFO, self.jabberID, "Fetching IQ-based avatar") highestActive = self.highestResource() if not highestActive: return d = self.sendIQAvatarRequest(to=self.jabberID+"/"+highestActive, fro=config.jid) d.addCallback(iqAvatarReceived) d.addErrback(errback) def updateNickname(self, nickname): self.nickname = nickname if not self.nickname: j = internJID(self.jabberID) self.nickname = j.user self.setStatus(self.show, self.status, self.url) def updateDescription(self, description): self.description = description if not self.description: self.description = "I am a PyAIM-t user with no profile set." def setStatus(self, show, status, url=None): self.show = show self.status = status self.url = url self.legacycon.setStatus(self.nickname, show, status, url) def sendNotReadyError(self, source, resource, dest, body): self.sendErrorMessage(source + '/' + resource, dest, "wait", "not-allowed", lang.get("waitforlogin", self.lang), body) def findGroupchat(self, to): pos = to.find('@') if pos > 0: roomID = to[:pos] else: roomID = to for groupchat in self.groupchats: if groupchat.ID == roomID: return groupchat return None def 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, avatarType="vcard"): if config.disableAvatars: return 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): av = self.pytrans.avatarCache.getAvatar(avatarHash) if av: self.avatar = av # Stuff in the cache is always PNG self.legacycon.updateAvatar(self.avatar) else: if avatarType == "vcard" and not config.disableVCardAvatars: self.doVCardUpdate() elif avatarType == "iq" and not config.disableIQAvatars: self.doIQAvatarUpdate() def messageReceived(self, source, resource, dest, destr, mtype, body, noerror, xhtml, autoResponse=0): if dest == config.jid: if body.lower().startswith("end"): LogEvent(INFO, self.jabberID, "Received 'end' request") self.removeMe() return if not self.ready: self.sendNotReadyError(source, resource, dest, body) return groupchat = self.findGroupchat(dest) if groupchat: # It's for a groupchat if destr and len(destr) > 0 and not noerror: self.sendErrorMessage(to=(source + "/" + resource), fro=dest, etype="cancel", condition="not-allowed", explanation=lang.get("groupchatprivateerror", self.lang), body=body) else: groupchat.sendMessage(body, noerror) else: LogEvent(INFO, self.jabberID, "Message") self.legacycon.sendMessage(dest, resource, body, noerror, xhtml, autoResponse=autoResponse) def inviteReceived(self, source, resource, dest, destr, roomjid): if not self.ready: self.sendNotReadyError(source, resource, dest, roomjid) return if not roomjid.endswith('@' + config.confjid): # Inviting an AIM user to a Jabber chatroom message = lang.get("groupchatadvocacy", self.lang) % (self.jabberID, config.website) self.legacycon.sendMessage(dest, resource, message, True, None) return groupchat = self.findGroupchat(roomjid) if groupchat: LogEvent(INFO, self.jabberID) groupchat.sendContactInvite(dest) def typingNotificationReceived(self, dest, resource, composing): """ The user has sent typing notification to a contact on the legacy service """ self.legacycon.userTypingNotification(dest, resource, composing) def chatStateReceived(self, dest, resource, state): """ The user has sent a chat state notification to a contact on the legacy service """ self.legacycon.chatStateNotification(dest, resource, state) def presenceReceived(self, source, resource, to, tor, priority, ptype, show, status, url=None): # Checks resources and priorities so that the highest priority resource always appears as the # legacy services status. If there are no more resources then the session is deleted # Additionally checks if the presence is to a groupchat room groupchat = self.findGroupchat(to) if groupchat: # It's for a groupchat if ptype == "unavailable": # Kill the groupchat LogEvent(INFO, self.jabberID, "Killing groupchat") groupchat.removeMe() else: if source == self.jabberID: if ptype == "error": groupchat.removeMe() else: groupchat.userJoined(tor) else: LogEvent(INFO, self.jabberID, "User not allowed") self.sendPresence(to=(source + "/" + resource), fro=to, ptype="error") elif legacy.isGroupJID(to) and to != config.confjid 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 LogEvent(INFO, self.jabberID, "Creating groupchat") groupchat = legacy.LegacyGroupchat(self, resource, gcID) # Creates an empty groupchat groupchat.userJoined(tor) elif ptype == 'probe': LogEvent(INFO, self.jabberID, "Responding to presence probe") if to == config.jid: self.legacycon.sendShowStatus(source) else: self.contactList.getContact(to).sendPresence(source) else: self.handleResourcePresence(source, resource, to, tor, priority, ptype, show, status, url) def handleResourcePresence(self, source, resource, to, tor, priority, ptype, show, status, url): 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: 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: LogEvent(INFO, self.jabberID, "Resource came online") self.contactList.resendLists("%s/%s"%(source,resource)) LogEvent(INFO, self.jabberID, "Setting status") self.resourceList[resource] = SessionResource(show, status, priority, url) highestActive = self.highestResource() if highestActive: # If we're the highest active resource, we should update the legacy service LogEvent(INFO, self.jabberID, "Updating status on legacy service") r = self.resourceList[highestActive] self.setStatus(r.show, r.status, r.url) else: 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: highestActive = checkR #if highestActive: # debug.log("Session %r - highest active resource is \"%r\" at %d" % (self.jabberID, highestActive, self.resourceList[highestActive].priority)) return highestActive def resourceOffline(self, resource): del self.resourceList[resource] self.legacycon.resourceOffline(resource) def subscriptionReceived(self, to, subtype): """ Sends the subscription request to the legacy services handler """ 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 register") self.pytrans.registermanager.removeRegInfo(jid) LogEvent(INFO, jid, "Just unregistered") class SessionResource: """ A convienence class to allow comparisons of Jabber resources """ def __init__(self, show=None, status=None, priority=None, url=None): self.show = show self.status = status self.priority = 0 self.url = url try: self.priority = int(priority) except TypeError: pass except ValueError: pass