]> code.delx.au - pymsnt/blob - src/legacy/glue.py
Getting there..
[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, 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
189 LogEvent(INFO, self.roomJID())
190
191 def removeMe(self):
192 del self.switchboardSession
193 groupchat.BaseGroupchat.removeMe(self)
194 LogEvent(INFO, self.roomJID())
195
196 def sendLegacyMessage(self, message, noerror):
197 LogEvent(INFO, self.roomJID())
198 self.switchboardSession.sendMessage(message.replace("\n", "\r\n"), noerror)
199
200 def sendContactInvite(self, contactJID):
201 LogEvent(INFO, self.roomJID())
202 userHandle = jid2msn(contactJID)
203 self.switchboardSession.inviteUser(userHandle)
204
205 def gotMessage(self, userHandle, text):
206 LogEvent(INFO, self.roomJID())
207 self.messageReceived(userHandle, text)
208
209
210
211 # This class handles most interaction with the legacy protocol
212 class LegacyConnection(msn.MSNConnection):
213 """ A glue class that connects to the legacy network """
214 def __init__(self, username, password, session):
215 self.session = session
216 self.listSynced = False
217 self.initialListVersion = 0
218
219 self.remoteShow = ""
220 self.remoteStatus = ""
221 self.remoteNick = ""
222
223 # Init the MSN bits
224 msn.MSNConnection.__init__(self, username, password, self.session.jabberID)
225
226 # User typing notification stuff
227 self.userTyping = dict() # Indexed by contact MSN ID, stores whether the user is typing to this contact
228 # Contact typing notification stuff
229 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
230 # Looping function
231 self.userTypingSend = task.LoopingCall(self.sendTypingNotifications)
232 self.userTypingSend.start(5.0)
233
234 self.legacyList = LegacyList(self.session)
235
236 LogEvent(INFO, self.session.jabberID)
237
238 def removeMe(self):
239 LogEvent(INFO, self.session.jabberID)
240
241 self.userTypingSend.stop()
242
243 self.legacyList.removeMe()
244 self.legacyList = None
245 self.session = None
246
247 def _sendShowStatus(self):
248 if not self.session: return
249 source = config.jid
250 to = self.session.jabberID
251 self.session.sendPresence(to=to, fro=source, show=self.remoteShow, status=self.remoteStatus, nickname=self.remoteNick)
252
253
254
255 # Implemented from baseproto
256 def resourceOffline(self, resource):
257 pass
258
259 def highestResource(self):
260 """ Returns highest priority resource """
261 return self.session.highestResource()
262
263 def sendMessage(self, dest, resource, body, noerror):
264 dest = jid2msn(dest)
265 if self.userTyping.has_key(dest):
266 del self.userTyping[dest]
267 try:
268 msn.MSNConnection.sendMessage(self, dest, body, noerror)
269 self.session.pytrans.statistics.stats["MessageCount"] += 1
270 except:
271 self.failedMessage(dest, body)
272 raise
273
274 def setStatus(self, nickname, show, status):
275 statusCode = presence2state(show, None)
276 msn.MSNConnection.changeStatus(self, statusCode, nickname, status)
277
278 def updateAvatar(self, av=None):
279 global defaultJabberAvatarData
280
281 if av:
282 msn.MSNConnection.changeAvatar(self, av.getImageData())
283 else:
284 msn.MSNConnection.changeAvatar(self, defaultJabberAvatarData)
285
286 def sendTypingNotifications(self):
287 if not self.session: return
288
289 # Send any typing notification messages to the user's contacts
290 for contact in self.userTyping.keys():
291 if self.userTyping[contact]:
292 self.sendTypingToContact(contact)
293
294 # Send any typing notification messages from contacts to the user
295 for contact in self.contactTyping.keys():
296 self.contactTyping[contact] += 1
297 if self.contactTyping[contact] >= 3:
298 self.session.sendTypingNotification(self.session.jabberID, msn2jid(contact), False)
299 del self.contactTyping[contact]
300
301 def userTypingNotification(self, dest, resource, composing):
302 if not self.session: return
303 dest = jid2msn(dest)
304 self.userTyping[dest] = composing
305 if composing: # Make it instant
306 self.sendTypingToContact(dest)
307
308
309
310 # Implement callbacks from msn.MSNConnection
311 def connectionFailed(self, reason):
312 LogEvent(INFO, self.session.jabberID)
313 text = lang.get(self.session.lang).msnConnectFailed % reason
314 self.session.sendMessage(to=self.session.jabberID, fro=config.jid, body=text)
315 self.session.removeMe()
316
317 def loginFailed(self, reason):
318 LogEvent(INFO, self.session.jabberID)
319 text = lang.get(self.session.lang).msnLoginFailure % (self.session.username)
320 self.session.sendErrorMessage(to=self.session.jabberID, fro=config.jid, etype="auth", condition="not-authorized", explanation=text, body="Login Failure")
321 self.session.removeMe()
322
323 def connectionLost(self, reason):
324 LogEvent(INFO, self.session.jabberID)
325 text = lang.get(self.session.lang).msnDisconnected % reason
326 self.session.sendMessage(to=self.session.jabberID, fro=config.jid, body=text)
327 self.session.removeMe() # Tear down the session
328
329 def multipleLogin(self):
330 LogEvent(INFO, self.session.jabberID)
331 self.session.sendMessage(to=self.session.jabberID, fro=config.jid, body=lang.get(self.session.lang).msnMultipleLogin)
332 self.session.removeMe()
333
334 def serverGoingDown(self):
335 LogEvent(INFO, self.session.jabberID)
336 self.session.sendMessage(to=self.session.jabberID, fro=config.jid, body=lang.get(self.session.lang).msnMaintenance)
337
338 def accountNotVerified(self):
339 LogEvent(INFO, self.session.jabberID)
340 text = lang.get(self.session.lang).msnNotVerified % (self.session.username)
341 self.session.sendMessage(to=self.session.jabberID, fro=config.jid, body=text)
342
343 def userMapping(self, passport, jid):
344 LogEvent(INFO, self.session.jabberID)
345 text = lang.get(self.session.lang).userMapping % (passport, jid)
346 self.session.sendMessage(to=self.session.jabberID, fro=msn2jid(passport), body=text)
347
348 def loggedIn(self):
349 LogEvent(INFO, self.session.jabberID)
350 self.session.ready = True
351
352 def listSynchronized(self):
353 LogEvent(INFO, self.session.jabberID)
354 self.session.sendPresence(to=self.session.jabberID, fro=config.jid)
355 self.legacyList.syncJabberLegacyLists()
356 self.listSynced = True
357 #self.legacyList.flushSubscriptionBuffer()
358
359 def ourStatusChanged(self, statusCode, screenName, personal):
360 # Send out a new presence packet to the Jabber user so that the transport icon changes
361 LogEvent(INFO, self.session.jabberID)
362 self.remoteShow, ptype = state2presence(statusCode)
363 self.remoteStatus = personal
364 self.remoteNick = screenName
365 self._sendShowStatus()
366
367 def gotMessage(self, remoteUser, text):
368 LogEvent(INFO, self.session.jabberID)
369 source = msn2jid(remoteUser)
370 self.session.sendMessage(self.session.jabberID, fro=source, body=text, mtype="chat")
371 self.session.pytrans.statistics.stats["MessageCount"] += 1
372
373 def gotGroupchat(self, msnGroupchat):
374 LogEvent(INFO, self.session.jabberID)
375 msnGroupchat.groupchat = LegacyGroupchat(self.session, switchboardSession=msnGroupchat)
376
377 def gotContactTyping(self, contact):
378 LogEvent(INFO, self.session.jabberID)
379 # Check if the contact has only just started typing
380 if not self.contactTyping.has_key(contact):
381 self.session.sendTypingNotification(self.session.jabberID, msn2jid(contact), True)
382
383 # Reset the counter
384 self.contactTyping[contact] = 0
385
386 def failedMessage(self, remoteUser, message):
387 LogEvent(INFO, self.session.jabberID)
388 self.session.pytrans.statistics.stats["FailedMessageCount"] += 1
389 fro = msn2jid(remoteUser)
390 self.session.sendErrorMessage(to=self.session.jabberID, fro=fro, etype="wait", condition="recipient-unavailable", explanation=lang.get(self.session.lang).msnFailedMessage, body=message)
391
392 def contactAvatarChanged(self, userHandle, hash):
393 LogEvent(INFO, self.session.jabberID)
394 jid = msn2jid(userHandle)
395 c = self.session.contactList.findContact(jid)
396 if not c: return
397
398 if hash:
399 # New avatar
400 av = self.session.pytrans.avatarCache.getAvatar(hash)
401 if av:
402 msnContact = self.getContacts().getContact(userHandle)
403 msnContact.msnobjGot = True
404 c.updateAvatar(av)
405 else:
406 def updateAvatar((imageData,)):
407 av = self.session.pytrans.avatarCache.setAvatar(imageData)
408 c.updateAvatar(av)
409 d = self.sendAvatarRequest(userHandle)
410 if d:
411 d.addCallback(updateAvatar)
412 else:
413 # They've turned off their avatar
414 global defaultAvatar
415 c.updateAvatar(defaultAvatar)
416
417 def contactStatusChanged(self, remoteUser):
418 LogEvent(INFO, self.session.jabberID)
419
420 msnContact = self.getContacts().getContact(remoteUser)
421 c = self.session.contactList.findContact(msn2jid(remoteUser))
422 if not (c and msnContact): return
423
424 show, ptype = state2presence(msnContact.status)
425 status = msnContact.personal.decode("utf-8")
426 screenName = msnContact.screenName.decode("utf-8")
427
428 c.updateNickname(screenName, push=False)
429 c.updatePresence(show, status, ptype, force=True)
430
431 def gotFileReceive(self, fileReceive):
432 LogEvent(INFO, self.session.jabberID)
433 pass # FIXME
434
435 def contactAddedMe(self, userHandle):
436 LogEvent(INFO, self.session.jabberID)
437 self.session.contactList.getContact(msn2jid(userHandle)).contactRequestsAuth()
438
439 def contactRemovedMe(self, userHandle):
440 LogEvent(INFO, self.session.jabberID)
441 c = self.session.contactList.getContact(msn2jid(userHandle))
442 c.contactDerequestsAuth()
443 c.contactRemovesAuth()
444
445 def gotInitialEmailNotification(self, inboxunread, foldersunread):
446 LogEvent(INFO, self.session.jabberID)
447 text = lang.get(self.session.lang).msnInitialMail % (inboxunread, foldersunread)
448 self.session.sendMessage(to=self.session.jabberID, fro=config.jid, body=text, mtype="headline")
449
450 def gotRealtimeEmailNotification(self, mailfrom, fromaddr, subject):
451 LogEvent(INFO, self.session.jabberID)
452 text = lang.get(self.session.lang).msnRealtimeMail % (mailfrom, fromaddr, subject)
453 self.session.sendMessage(to=self.session.jabberID, fro=config.jid, body=text, mtype="headline")
454
455 def gotMSNAlert(self, text, actionurl, subscrurl):
456 LogEvent(INFO, self.session.jabberID)
457
458 el = Element((None, "message"))
459 el.attributes["to"] = self.session.jabberID
460 el.attributes["from"] = config.jid
461 el.attributes["type"] = "headline"
462 body = el.addElement("body")
463 body.addContent(text)
464
465 x = el.addElement("x")
466 x.attributes["xmlns"] = "jabber:x:oob"
467 x.addElement("desc").addContent("More information on this notice.")
468 x.addElement("url").addContent(actionurl)
469
470 x = el.addElement("x")
471 x.attributes["xmlns"] = "jabber:x:oob"
472 x.addElement("desc").addContent("Manage subscriptions to alerts.")
473 x.addElement("url").addContent(subscrurl)
474
475 self.session.pytrans.send(el)
476
477
478
479
480 class LegacyList:
481 def __init__(self, session):
482 self.session = session
483 self.subscriptionBuffer = []
484
485 def removeMe(self):
486 self.subscriptionBuffer = None
487 self.session = None
488
489 def addContact(self, jid):
490 LogEvent(INFO, self.session.jabberID)
491 userHandle = jid2msn(jid)
492 self.session.legacycon.addContact(msn.FORWARD_LIST, userHandle)
493 self.session.contactList.getContact(jid).contactGrantsAuth()
494
495 def removeContact(self, jid):
496 LogEvent(INFO, self.session.jabberID)
497 jid = jid2msn(jid)
498 self.session.legacycon.remContact(msn.FORWARD_LIST, jid)
499
500
501 def authContact(self, jid):
502 LogEvent(INFO, self.session.jabberID)
503 jid = jid2msn(jid)
504 d = self.session.legacycon.remContact(msn.PENDING_LIST, jid)
505 if d:
506 self.session.legacycon.addContact(msn.REVERSE_LIST, jid)
507 self.session.legacycon.remContact(msn.BLOCK_LIST, jid)
508 self.session.legacycon.addContact(msn.ALLOW_LIST, jid)
509
510 def deauthContact(self, jid):
511 LogEvent(INFO, self.session.jabberID)
512 jid = jid2msn(jid)
513 self.session.legacycon.remContact(msn.ALLOW_LIST, jid)
514 self.session.legacycon.addContact(msn.BLOCK_LIST, jid)
515
516
517
518 def syncJabberLegacyLists(self):
519 """ Synchronises the MSN contact list on server with the Jabber contact list """
520
521 global defaultAvatar
522
523 # We have to make an MSNContactList from the XDB data, then compare it with the one the server sent
524 # Any subscription changes must be sent to the client, as well as changed in the XDB
525 LogEvent(INFO, self.session.jabberID, "Start.")
526 result = self.session.pytrans.xdb.request(self.session.jabberID, disco.IQROSTER)
527 oldContactList = msn.MSNContactList()
528 if result:
529 for item in result.elements():
530 user = item.getAttribute("jid")
531 sub = item.getAttribute("subscription")
532 lists = item.getAttribute("lists")
533 if not lists:
534 lists = jabsub2msnlist(sub) # Backwards compatible
535 lists = int(lists)
536 contact = msn.MSNContact(userHandle=user, screenName="", lists=lists)
537 oldContactList.addContact(contact)
538
539 newXDB = Element((None, "query"))
540 newXDB.attributes["xmlns"] = disco.IQROSTER
541
542 contactList = self.session.legacycon.getContacts()
543
544
545 # Convienence functions
546 def addedToList(num):
547 return (not (oldLists & num) and (lists & num))
548 def removedFromList(num):
549 return ((oldLists & num) and not (lists & num))
550
551 for contact in contactList.contacts.values():
552 # Compare with the XDB <item/> entry
553 oldContact = oldContactList.getContact(contact.userHandle)
554 if oldContact == None:
555 oldLists = 0
556 else:
557 oldLists = oldContact.lists
558 lists = contact.lists
559
560 # Create the Jabber representation of the
561 # contact base on the old list data and then
562 # sync it with current
563 jabContact = self.session.contactList.createContact(msn2jid(contact.userHandle), msnlist2jabsub(oldLists))
564 jabContact.updateAvatar(defaultAvatar, push=False)
565
566 if addedToList(msn.FORWARD_LIST):
567 jabContact.syncGroups(getGroupNames(contact, contactList), push=False)
568 jabContact.syncContactGrantedAuth()
569
570 if removedFromList(msn.FORWARD_LIST):
571 jabContact.syncContactRemovedAuth()
572
573 if addedToList(msn.ALLOW_LIST):
574 jabContact.syncUserGrantedAuth()
575
576 if addedToList(msn.BLOCK_LIST) or removedFromList(msn.ALLOW_LIST):
577 jabContact.syncUserRemovedAuth()
578
579 if (not (lists & msn.ALLOW_LIST) and not (lists & msn.BLOCK_LIST) and (lists & msn.REVERSE_LIST)) or (lists & msn.PENDING_LIST):
580 jabContact.contactRequestsAuth()
581
582 if removedFromList(msn.REVERSE_LIST):
583 jabContact.contactDerequestsAuth()
584
585 item = newXDB.addElement("item")
586 item.attributes["jid"] = contact.userHandle
587 item.attributes["subscription"] = msnlist2jabsub(lists)
588 item.attributes["lists"] = str(lists)
589
590 # Update the XDB
591 self.session.pytrans.xdb.set(self.session.jabberID, disco.IQROSTER, newXDB)
592 LogEvent(INFO, self.session.jabberID, "End.")
593
594 def saveLegacyList(self):
595 contactList = self.session.legacycon.getContacts()
596 if not contactList: return
597
598 newXDB = Element((None, "query"))
599 newXDB.attributes["xmlns"] = disco.IQROSTER
600
601 for contact in contactList.contacts.values():
602 item = newXDB.addElement("item")
603 item.attributes["jid"] = contact.userHandle
604 item.attributes["subscription"] = msnlist2jabsub(contact.lists) # Backwards compat
605 item.attributes["lists"] = str(contact.lists)
606
607 self.session.pytrans.xdb.set(self.session.jabberID, disco.IQROSTER, newXDB)
608 LogEvent(INFO, self.session.jabberID, "Finished saving list.")
609
610