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