]> code.delx.au - pymsnt/blob - src/legacy/glue.py
Major changes. Moved everything away from twistd and .tac files. Its nicer this way...
[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 reloadConfig():
39 msn.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