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