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