]> code.delx.au - pymsnt/blob - src/legacy/glue.py
Committed Remko's patches (env & typing notification fix)
[pymsnt] / src / legacy / glue.py
1 # Copyright 2004-2005 James Bunton <james@delx.cjb.net>
2 # Licensed for distribution under the GPL version 2, check COPYING for details
3
4 import os.path
5 import utils
6 from twisted.internet import task
7 from tlib.xmlw import Element
8 from tlib import msn
9 from debug import LogEvent, INFO, WARN, ERROR
10 import disco
11 import groupchat
12 import ft
13 import avatar
14 import config
15 import lang
16
17
18
19
20 name = "MSN Transport" # The name of the transport
21 url = "http://msn-transport.jabberstudio.org"
22 version = "0.11-dev" # The transport version
23 mangle = True # XDB '@' -> '%' mangling
24 id = "msn" # The transport identifier
25
26
27 # Load the default avatars
28 f = open(os.path.join("data", "defaultJabberAvatar.png"), "rb")
29 defaultJabberAvatarData = f.read()
30 f.close()
31
32 f = open(os.path.join("data", "defaultMSNAvatar.png"), "rb")
33 defaultAvatarData = f.read()
34 f.close()
35 defaultAvatar = avatar.AvatarCache().setAvatar(defaultAvatarData)
36
37
38 def reloadConfig():
39 msn.MSNConnection.GETALLAVATARS = config.getAllAvatars
40
41 def isGroupJID(jid):
42 """ Returns True if the JID passed is a valid groupchat JID (for MSN, does not contain '%') """
43 return (jid.find('%') == -1)
44
45
46
47 # This should be set to the name space the registration entries are in, in the xdb spool
48 namespace = "jabber:iq:register"
49
50
51 def formRegEntry(username, password):
52 """ Returns a domish.Element representation of the data passed. This element will be written to the XDB spool file """
53 reginfo = Element((None, "query"))
54 reginfo.attributes["xmlns"] = "jabber:iq:register"
55
56 userEl = reginfo.addElement("username")
57 userEl.addContent(username)
58
59 passEl = reginfo.addElement("password")
60 passEl.addContent(password)
61
62 return reginfo
63
64
65
66
67 def getAttributes(base):
68 """ This function should, given a spool domish.Element, pull the username, password,
69 and out of it and return them """
70 username = ""
71 password = ""
72 for child in base.elements():
73 try:
74 if child.name == "username":
75 username = child.__str__()
76 elif child.name == "password":
77 password = child.__str__()
78 except AttributeError:
79 continue
80
81 return username, password
82
83
84 def startStats(statistics):
85 stats = statistics.stats
86 stats["MessageCount"] = 0
87 stats["FailedMessageCount"] = 0
88 stats["AvatarCount"] = 0
89 stats["FailedAvatarCount"] = 0
90
91 def updateStats(statistics):
92 stats = statistics.stats
93 # FIXME
94 #stats["AvatarCount"] = msnp2p.MSNP2P_Avatar.TRANSFER_COUNT
95 #stats["FailedAvatarCount"] = msnp2p.MSNP2P_Avatar.ERROR_COUNT
96
97
98 def msn2jid(msnid, withResource):
99 """ Converts a MSN passport into a JID representation to be used with the transport """
100 return msnid.replace('@', '%') + "@" + config.jid + (withResource and "/msn" or "")
101
102 # Marks this as the function to be used in jabber:iq:gateway (Service ID Translation)
103 translateAccount = lambda a: msn2jid(a, False)
104
105 def jid2msn(jid):
106 """ Converts a JID representation of a MSN passport into the original MSN passport """
107 return unicode(jid[:jid.find('@')].replace('%', '@')).split("/")[0]
108
109
110 def presence2state(show, ptype):
111 """ Converts a Jabber presence into an MSN status code """
112 if ptype == "unavailable":
113 return msn.STATUS_OFFLINE
114 elif not show or show == "online" or show == "chat":
115 return msn.STATUS_ONLINE
116 elif show == "dnd":
117 return msn.STATUS_BUSY
118 elif show == "away" or show == "xa":
119 return msn.STATUS_AWAY
120
121
122 def state2presence(state):
123 """ Converts a MSN status code into a Jabber presence """
124 if state == msn.STATUS_ONLINE:
125 return (None, None)
126 elif state == msn.STATUS_BUSY:
127 return ("dnd", None)
128 elif state == msn.STATUS_AWAY:
129 return ("away", None)
130 elif state == msn.STATUS_IDLE:
131 return ("away", None)
132 elif state == msn.STATUS_BRB:
133 return ("away", None)
134 elif state == msn.STATUS_PHONE:
135 return ("dnd", None)
136 elif state == msn.STATUS_LUNCH:
137 return ("away", None)
138 else:
139 return (None, "unavailable")
140
141
142 def getGroupNames(msnContact, msnContactList):
143 """ Gets a list of groups that this contact is in """
144 groups = []
145 for groupGUID in msnContact.groups:
146 try:
147 groups.append(msnContactList.groups[groupGUID])
148 except KeyError:
149 pass
150 return groups
151
152 def msnlist2jabsub(lists):
153 """ Converts MSN contact lists ORed together into the corresponding Jabber subscription state """
154 if lists & msn.FORWARD_LIST and lists & msn.REVERSE_LIST:
155 return "both"
156 elif lists & msn.REVERSE_LIST:
157 return "from"
158 elif lists & msn.FORWARD_LIST:
159 return "to"
160 else:
161 return "none"
162
163
164 def jabsub2msnlist(sub):
165 """ Converts a Jabber subscription state into the corresponding MSN contact lists ORed together """
166 if sub == "to":
167 return msn.FORWARD_LIST
168 elif sub == "from":
169 return msn.REVERSE_LIST
170 elif sub == "both":
171 return (msn.FORWARD_LIST | msn.REVERSE_LIST)
172 else:
173 return 0
174
175
176
177
178
179 # This class handles groupchats with the legacy protocol
180 class LegacyGroupchat(groupchat.BaseGroupchat):
181 def __init__(self, session, resource=None, ID=None, switchboardSession=None):
182 """ Possible entry points for groupchat
183 - User starts an empty switchboard session by sending presence to a blank room
184 - An existing switchboard session is joined by another MSN user
185 - User invited to an existing switchboard session with more than one user
186 """
187 groupchat.BaseGroupchat.__init__(self, session, resource, ID)
188 if switchboardSession:
189 self.switchboardSession = switchboardSession
190 else:
191 self.switchboardSession = msn.MultiSwitchboardSession(self.session.legacycon)
192 self.switchboardSession.groupchat = self
193
194 LogEvent(INFO, self.roomJID())
195
196 def removeMe(self):
197 if self.switchboardSession.transport:
198 self.switchboardSession.transport.loseConnection()
199 self.switchboardSession.groupchat = None
200 del self.switchboardSession
201 groupchat.BaseGroupchat.removeMe(self)
202 LogEvent(INFO, self.roomJID())
203
204 def sendLegacyMessage(self, message, noerror):
205 LogEvent(INFO, self.roomJID())
206 self.switchboardSession.sendMessage(message.replace("\n", "\r\n"), noerror)
207
208 def sendContactInvite(self, contactJID):
209 LogEvent(INFO, self.roomJID())
210 userHandle = jid2msn(contactJID)
211 self.switchboardSession.inviteUser(userHandle)
212
213 def gotMessage(self, userHandle, text):
214 LogEvent(INFO, self.roomJID())
215 self.messageReceived(userHandle, text)
216
217
218
219 # This class handles most interaction with the legacy protocol
220 class LegacyConnection(msn.MSNConnection):
221 """ A glue class that connects to the legacy network """
222 def __init__(self, username, password, session):
223 self.jabberID = session.jabberID
224
225 self.session = session
226 self.listSynced = False
227 self.initialListVersion = 0
228
229 self.remoteShow = ""
230 self.remoteStatus = ""
231 self.remoteNick = ""
232
233 # Init the MSN bits
234 msn.MSNConnection.__init__(self, username, password, self.jabberID)
235
236 # User typing notification stuff
237 self.userTyping = dict() # Indexed by contact MSN ID, stores whether the user is typing to this contact
238 # Contact typing notification stuff
239 self.contactTyping = dict() # Indexed by contact MSN ID, stores an integer that is incremented at 5 second intervals. If it reaches 3 then the contact has stopped typing. It is set to zero whenever MSN typing notification messages are received
240 # Looping function
241 self.userTypingSend = task.LoopingCall(self.sendTypingNotifications)
242 self.userTypingSend.start(5.0)
243
244 self.legacyList = LegacyList(self.session)
245
246 LogEvent(INFO, self.jabberID)
247
248 def removeMe(self):
249 LogEvent(INFO, self.jabberID)
250
251 self.userTypingSend.stop()
252
253 self.legacyList.removeMe()
254 self.legacyList = None
255 self.session = None
256 self.logOut()
257
258
259 # Implemented from baseproto
260 def sendShowStatus(self, jid=None):
261 if not self.session: return
262 source = config.jid
263 if not jid:
264 jid = self.jabberID
265 self.session.sendPresence(to=jid, fro=source, show=self.remoteShow, status=self.remoteStatus, nickname=self.remoteNick)
266
267 def resourceOffline(self, resource):
268 pass
269
270 def highestResource(self):
271 """ Returns highest priority resource """
272 return self.session.highestResource()
273
274 def sendMessage(self, dest, resource, body, noerror):
275 dest = jid2msn(dest)
276 if self.userTyping.has_key(dest):
277 del self.userTyping[dest]
278 try:
279 msn.MSNConnection.sendMessage(self, dest, body, noerror)
280 self.session.pytrans.statistics.stats["MessageCount"] += 1
281 except:
282 self.failedMessage(dest, body)
283 raise
284
285 def sendFile(self, dest, ftSend):
286 dest = jid2msn(dest)
287 def continueSendFile1((msnFileSend, d)):
288 def continueSendFile2((success, )):
289 if success:
290 ftSend.accept(msnFileSend)
291 else:
292 sendFileFail()
293 d.addCallbacks(continueSendFile2, sendFileFail)
294
295 def sendFileFail():
296 ftSend.reject()
297
298 d = msn.MSNConnection.sendFile(self, dest, ftSend.filename, ftSend.filesize)
299 d.addCallbacks(continueSendFile1, sendFileFail)
300
301 def setStatus(self, nickname, show, status):
302 statusCode = presence2state(show, None)
303 msn.MSNConnection.changeStatus(self, statusCode, nickname, status)
304
305 def updateAvatar(self, av=None):
306 global defaultJabberAvatarData
307
308 if av:
309 msn.MSNConnection.changeAvatar(self, av.getImageData())
310 else:
311 msn.MSNConnection.changeAvatar(self, defaultJabberAvatarData)
312
313 def sendTypingNotifications(self):
314 if not self.session: return
315
316 # Send any typing notification messages to the user's contacts
317 for contact in self.userTyping.keys():
318 if self.userTyping[contact]:
319 self.sendTypingToContact(contact)
320
321 # Send any typing notification messages from contacts to the user
322 for contact in self.contactTyping.keys():
323 self.contactTyping[contact] += 1
324 if self.contactTyping[contact] >= 3:
325 self.session.sendTypingNotification(self.jabberID, msn2jid(contact, True), False)
326 del self.contactTyping[contact]
327
328 def userTypingNotification(self, dest, resource, composing):
329 if not self.session: return
330 dest = jid2msn(dest)
331 self.userTyping[dest] = composing
332 if composing: # Make it instant
333 self.sendTypingToContact(dest)
334
335
336
337 # Implement callbacks from msn.MSNConnection
338 def connectionFailed(self, reason):
339 LogEvent(INFO, self.jabberID)
340 text = lang.get(self.session.lang).msnConnectFailed % reason
341 self.session.sendMessage(to=self.jabberID, fro=config.jid, body=text)
342 self.session.removeMe()
343
344 def loginFailed(self, reason):
345 LogEvent(INFO, self.jabberID)
346 text = lang.get(self.session.lang).msnLoginFailure % (self.session.username)
347 self.session.sendErrorMessage(to=self.jabberID, fro=config.jid, etype="auth", condition="not-authorized", explanation=text, body="Login Failure")
348 self.session.removeMe()
349
350 def connectionLost(self, reason):
351 LogEvent(INFO, self.jabberID)
352 text = lang.get(self.session.lang).msnDisconnected % reason
353 self.session.sendMessage(to=self.jabberID, fro=config.jid, body=text)
354 self.session.removeMe() # Tear down the session
355
356 def multipleLogin(self):
357 LogEvent(INFO, self.jabberID)
358 self.session.sendMessage(to=self.jabberID, fro=config.jid, body=lang.get(self.session.lang).msnMultipleLogin)
359 self.session.removeMe()
360
361 def serverGoingDown(self):
362 LogEvent(INFO, self.jabberID)
363 self.session.sendMessage(to=self.jabberID, fro=config.jid, body=lang.get(self.session.lang).msnMaintenance)
364
365 def accountNotVerified(self):
366 LogEvent(INFO, self.jabberID)
367 text = lang.get(self.session.lang).msnNotVerified % (self.session.username)
368 self.session.sendMessage(to=self.jabberID, fro=config.jid, body=text)
369
370 def userMapping(self, passport, jid):
371 LogEvent(INFO, self.jabberID)
372 text = lang.get(self.session.lang).userMapping % (passport, jid)
373 self.session.sendMessage(to=self.jabberID, fro=msn2jid(passport, True), body=text)
374
375 def loggedIn(self):
376 LogEvent(INFO, self.jabberID)
377 self.session.ready = True
378
379 def listSynchronized(self):
380 LogEvent(INFO, self.jabberID)
381 self.session.sendPresence(to=self.jabberID, fro=config.jid)
382 self.legacyList.syncJabberLegacyLists()
383 self.listSynced = True
384 #self.legacyList.flushSubscriptionBuffer()
385
386 def ourStatusChanged(self, statusCode, screenName, personal):
387 # Send out a new presence packet to the Jabber user so that the transport icon changes
388 LogEvent(INFO, self.jabberID)
389 self.remoteShow, ptype = state2presence(statusCode)
390 self.remoteStatus = personal
391 self.remoteNick = screenName
392 self.sendShowStatus()
393
394 def gotMessage(self, remoteUser, text):
395 LogEvent(INFO, self.jabberID)
396 source = msn2jid(remoteUser, True)
397 if self.contactTyping.has_key(remoteUser):
398 del self.contactTyping[remoteUser]
399 self.session.sendMessage(self.jabberID, fro=source, body=text, mtype="chat")
400 self.session.pytrans.statistics.stats["MessageCount"] += 1
401
402 def gotGroupchat(self, msnGroupchat, userHandle):
403 LogEvent(INFO, self.jabberID)
404 msnGroupchat.groupchat = LegacyGroupchat(self.session, switchboardSession=msnGroupchat)
405 msnGroupchat.groupchat.sendUserInvite(msn2jid(userHandle, True))
406
407 def gotContactTyping(self, contact):
408 LogEvent(INFO, self.jabberID)
409 # Check if the contact has only just started typing
410 if not self.contactTyping.has_key(contact):
411 self.session.sendTypingNotification(self.jabberID, msn2jid(contact, True), True)
412
413 # Reset the counter
414 self.contactTyping[contact] = 0
415
416 def failedMessage(self, remoteUser, message):
417 LogEvent(INFO, self.jabberID)
418 self.session.pytrans.statistics.stats["FailedMessageCount"] += 1
419 fro = msn2jid(remoteUser, True)
420 self.session.sendErrorMessage(to=self.jabberID, fro=fro, etype="wait", condition="recipient-unavailable", explanation=lang.get(self.session.lang).msnFailedMessage, body=message)
421
422 def contactAvatarChanged(self, userHandle, hash):
423 LogEvent(INFO, self.jabberID)
424 jid = msn2jid(userHandle, False)
425 c = self.session.contactList.findContact(jid)
426 if not c: return
427
428 if hash:
429 # New avatar
430 av = self.session.pytrans.avatarCache.getAvatar(hash)
431 if av:
432 msnContact = self.getContacts().getContact(userHandle)
433 msnContact.msnobjGot = True
434 c.updateAvatar(av)
435 else:
436 def updateAvatarCB((imageData,)):
437 av = self.session.pytrans.avatarCache.setAvatar(imageData)
438 c.updateAvatar(av)
439 d = self.sendAvatarRequest(userHandle)
440 if d:
441 d.addCallback(updateAvatarCB)
442 else:
443 # They've turned off their avatar
444 global defaultAvatar
445 c.updateAvatar(defaultAvatar)
446
447 def contactStatusChanged(self, remoteUser):
448 LogEvent(INFO, self.jabberID)
449
450 msnContact = self.getContacts().getContact(remoteUser)
451 c = self.session.contactList.findContact(msn2jid(remoteUser, False))
452 if not (c and msnContact): return
453
454 show, ptype = state2presence(msnContact.status)
455 status = msnContact.personal.decode("utf-8")
456 screenName = msnContact.screenName.decode("utf-8")
457
458 c.updateNickname(screenName, push=False)
459 c.updatePresence(show, status, ptype, force=True)
460
461 def gotFileReceive(self, fileReceive):
462 LogEvent(INFO, self.jabberID)
463 # FIXME
464 ft.FTReceive(self.session, msn2jid(fileReceive.userHandle, True), fileReceive)
465
466 def contactAddedMe(self, userHandle):
467 LogEvent(INFO, self.jabberID)
468 self.session.contactList.getContact(msn2jid(userHandle, False)).contactRequestsAuth()
469
470 def contactRemovedMe(self, userHandle):
471 LogEvent(INFO, self.jabberID)
472 c = self.session.contactList.getContact(msn2jid(userHandle, True))
473 c.contactDerequestsAuth()
474 c.contactRemovesAuth()
475
476 def gotInitialEmailNotification(self, inboxunread, foldersunread):
477 if config.mailNotifications:
478 LogEvent(INFO, self.jabberID)
479 text = lang.get(self.session.lang).msnInitialMail % (inboxunread, foldersunread)
480 self.session.sendMessage(to=self.jabberID, fro=config.jid, body=text, mtype="headline")
481
482 def gotRealtimeEmailNotification(self, mailfrom, fromaddr, subject):
483 if config.mailNotifications:
484 LogEvent(INFO, self.jabberID)
485 text = lang.get(self.session.lang).msnRealtimeMail % (mailfrom, fromaddr, subject)
486 self.session.sendMessage(to=self.jabberID, fro=config.jid, body=text, mtype="headline")
487
488 def gotMSNAlert(self, text, actionurl, subscrurl):
489 LogEvent(INFO, self.jabberID)
490
491 el = Element((None, "message"))
492 el.attributes["to"] = self.jabberID
493 el.attributes["from"] = config.jid
494 el.attributes["type"] = "headline"
495 body = el.addElement("body")
496 body.addContent(text)
497
498 x = el.addElement("x")
499 x.attributes["xmlns"] = "jabber:x:oob"
500 x.addElement("desc").addContent("More information on this notice.")
501 x.addElement("url").addContent(actionurl)
502
503 x = el.addElement("x")
504 x.attributes["xmlns"] = "jabber:x:oob"
505 x.addElement("desc").addContent("Manage subscriptions to alerts.")
506 x.addElement("url").addContent(subscrurl)
507
508 self.session.pytrans.send(el)
509
510 def gotAvatarImageData(self, userHandle, imageData):
511 LogEvent(INFO, self.jabberID)
512 av = self.session.pytrans.avatarCache.setAvatar(imageData)
513 jid = msn2jid(userHandle, False)
514 c = self.session.contactList.findContact(jid)
515 c.updateAvatar(av)
516
517
518
519
520 class LegacyList:
521 def __init__(self, session):
522 self.jabberID = session.jabberID
523 self.session = session
524 self.subscriptionBuffer = []
525
526 def removeMe(self):
527 self.subscriptionBuffer = None
528 self.session = None
529
530 def addContact(self, jid):
531 LogEvent(INFO, self.jabberID)
532 userHandle = jid2msn(jid)
533 self.session.legacycon.addContact(msn.FORWARD_LIST, userHandle)
534 self.session.contactList.getContact(jid).contactGrantsAuth()
535
536 def removeContact(self, jid):
537 LogEvent(INFO, self.jabberID)
538 jid = jid2msn(jid)
539 self.session.legacycon.remContact(msn.FORWARD_LIST, jid)
540
541
542 def authContact(self, jid):
543 LogEvent(INFO, self.jabberID)
544 jid = jid2msn(jid)
545 d = self.session.legacycon.remContact(msn.PENDING_LIST, jid)
546 if d:
547 self.session.legacycon.addContact(msn.REVERSE_LIST, jid)
548 self.session.legacycon.remContact(msn.BLOCK_LIST, jid)
549 self.session.legacycon.addContact(msn.ALLOW_LIST, jid)
550
551 def deauthContact(self, jid):
552 LogEvent(INFO, self.jabberID)
553 jid = jid2msn(jid)
554 self.session.legacycon.remContact(msn.ALLOW_LIST, jid)
555 self.session.legacycon.addContact(msn.BLOCK_LIST, jid)
556
557
558
559 def syncJabberLegacyLists(self):
560 """ Synchronises the MSN contact list on server with the Jabber contact list """
561
562 global defaultAvatar
563
564 # We have to make an MSNContactList from the XDB data, then compare it with the one the server sent
565 # Any subscription changes must be sent to the client, as well as changed in the XDB
566 LogEvent(INFO, self.jabberID, "Start.")
567 result = self.session.pytrans.xdb.request(self.jabberID, disco.IQROSTER)
568 oldContactList = msn.MSNContactList()
569 if result:
570 for item in result.elements():
571 user = item.getAttribute("jid")
572 sub = item.getAttribute("subscription")
573 lists = item.getAttribute("lists")
574 if not lists:
575 lists = jabsub2msnlist(sub) # Backwards compatible
576 lists = int(lists)
577 contact = msn.MSNContact(userHandle=user, screenName="", lists=lists)
578 oldContactList.addContact(contact)
579
580 newXDB = Element((None, "query"))
581 newXDB.attributes["xmlns"] = disco.IQROSTER
582
583 contactList = self.session.legacycon.getContacts()
584
585
586 # Convienence functions
587 def addedToList(num):
588 return (not (oldLists & num) and (lists & num))
589 def removedFromList(num):
590 return ((oldLists & num) and not (lists & num))
591
592 for contact in contactList.contacts.values():
593 # Compare with the XDB <item/> entry
594 oldContact = oldContactList.getContact(contact.userHandle)
595 if oldContact == None:
596 oldLists = 0
597 else:
598 oldLists = oldContact.lists
599 lists = contact.lists
600
601 # Create the Jabber representation of the
602 # contact base on the old list data and then
603 # sync it with current
604 jabContact = self.session.contactList.createContact(msn2jid(contact.userHandle, False), msnlist2jabsub(oldLists))
605 jabContact.updateAvatar(defaultAvatar, push=False)
606
607 if addedToList(msn.FORWARD_LIST):
608 jabContact.syncGroups(getGroupNames(contact, contactList), push=False)
609 jabContact.syncContactGrantedAuth()
610
611 if removedFromList(msn.FORWARD_LIST):
612 jabContact.syncContactRemovedAuth()
613
614 if addedToList(msn.ALLOW_LIST):
615 jabContact.syncUserGrantedAuth()
616
617 if addedToList(msn.BLOCK_LIST) or removedFromList(msn.ALLOW_LIST):
618 jabContact.syncUserRemovedAuth()
619
620 if (not (lists & msn.ALLOW_LIST) and not (lists & msn.BLOCK_LIST) and (lists & msn.REVERSE_LIST)) or (lists & msn.PENDING_LIST):
621 jabContact.contactRequestsAuth()
622
623 if removedFromList(msn.REVERSE_LIST):
624 jabContact.contactDerequestsAuth()
625
626 jabContact.syncRoster()
627
628 item = newXDB.addElement("item")
629 item.attributes["jid"] = contact.userHandle
630 item.attributes["subscription"] = msnlist2jabsub(lists)
631 item.attributes["lists"] = str(lists)
632
633 # Update the XDB
634 self.session.pytrans.xdb.set(self.jabberID, disco.IQROSTER, newXDB)
635 LogEvent(INFO, self.jabberID, "End.")
636
637 def saveLegacyList(self):
638 contactList = self.session.legacycon.getContacts()
639 if not contactList: return
640
641 newXDB = Element((None, "query"))
642 newXDB.attributes["xmlns"] = disco.IQROSTER
643
644 for contact in contactList.contacts.values():
645 item = newXDB.addElement("item")
646 item.attributes["jid"] = contact.userHandle
647 item.attributes["subscription"] = msnlist2jabsub(contact.lists) # Backwards compat
648 item.attributes["lists"] = str(contact.lists)
649
650 self.session.pytrans.xdb.set(self.jabberID, disco.IQROSTER, newXDB)
651 LogEvent(INFO, self.jabberID, "Finished saving list.")
652
653