]> code.delx.au - pymsnt/blob - src/legacy/glue.py
Partially working with new msnw
[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, existing=False, 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 not existing:
185 self.switchboardSession = msn.GroupchatSwitchboardSession(self, makeSwitchboard=True)
186 else:
187 self.switchboardSession = switchboardSession
188
189 assert(self.switchboardSession != None)
190
191 LogEvent(INFO, self.roomJID())
192
193 def removeMe(self):
194 self.switchboardSession.removeMe()
195 self.switchboardSession = None
196 groupchat.BaseGroupchat.removeMe(self)
197 LogEvent(INFO, self.roomJID())
198
199 def sendLegacyMessage(self, message, noerror):
200 LogEvent(INFO, self.roomJID())
201 self.switchboardSession.sendMessage(message.replace("\n", "\r\n"), noerror)
202
203 def sendContactInvite(self, contactJID):
204 LogEvent(INFO, self.roomJID())
205 userHandle = jid2msn(contactJID)
206 self.switchboardSession.inviteUser(userHandle)
207
208
209
210 # This class handles most interaction with the legacy protocol
211 class LegacyConnection(msn.MSNConnection):
212 """ A glue class that connects to the legacy network """
213 def __init__(self, username, password, session):
214 self.session = session
215 self.listSynced = False
216 self.initialListVersion = 0
217
218 self.remoteShow = ""
219 self.remoteStatus = ""
220 self.remoteNick = ""
221
222 # Init the MSN bits
223 msn.MSNConnection.__init__(self, username, password, self.session.jabberID)
224
225 # User typing notification stuff
226 self.userTyping = dict() # Indexed by contact MSN ID, stores whether the user is typing to this contact
227 # Contact typing notification stuff
228 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
229 # Looping function
230 self.userTypingSend = task.LoopingCall(self.sendTypingNotifications)
231 self.userTypingSend.start(5.0)
232
233 self.legacyList = LegacyList(self.session)
234
235 LogEvent(INFO, self.session.jabberID)
236
237 def removeMe(self):
238 LogEvent(INFO, self.session.jabberID)
239
240 self.userTypingSend.stop()
241
242 self.legacyList.removeMe()
243 self.legacyList = None
244 self.session = None
245
246 def resourceOffline(self, resource):
247 pass
248
249 def highestResource(self):
250 """ Returns highest priority resource """
251 return self.session.highestResource()
252
253 def sendMessage(self, dest, resource, body, noerror):
254 dest = jid2msn(dest)
255 if self.userTyping.has_key(dest):
256 del self.userTyping[dest]
257 try:
258 msn.MSNConnection.sendMessage(self, dest, body, noerror)
259 self.session.pytrans.statistics.stats["MessageCount"] += 1
260 except:
261 self.failedMessage(dest, body)
262 raise
263
264 def msnAlert(self, text, actionurl, subscrurl):
265 if not self.session: return
266
267 el = Element((None, "message"))
268 el.attributes["to"] = self.session.jabberID
269 el.attributes["from"] = config.jid
270 el.attributes["type"] = "headline"
271 body = el.addElement("body")
272 body.addContent(text)
273
274 x = el.addElement("x")
275 x.attributes["xmlns"] = "jabber:x:oob"
276 x.addElement("desc").addContent("More information on this notice.")
277 x.addElement("url").addContent(actionurl)
278
279 x = el.addElement("x")
280 x.attributes["xmlns"] = "jabber:x:oob"
281 x.addElement("desc").addContent("Manage subscriptions to alerts.")
282 x.addElement("url").addContent(subscrurl)
283
284 self.session.pytrans.send(el)
285
286 def setStatus(self, nickname, show, status):
287 statusCode = presence2state(show, None)
288 msn.MSNConnection.changeStatus(self, statusCode, nickname, status)
289
290 def updateAvatar(self, av=None):
291 global defaultJabberAvatarData
292
293 if av:
294 msn.MSNConnection.changeAvatar(self, av.getImageData())
295 else:
296 msn.MSNConnection.changeAvatar(self, defaultJabberAvatarData)
297
298 def sendTypingNotifications(self):
299 if not self.session: return
300
301 # Send any typing notification messages to the user's contacts
302 for contact in self.userTyping.keys():
303 if self.userTyping[contact]:
304 self.sendTypingToContact(contact)
305
306 # Send any typing notification messages from contacts to the user
307 for contact in self.contactTyping.keys():
308 self.contactTyping[contact] += 1
309 if self.contactTyping[contact] >= 3:
310 self.session.sendTypingNotification(self.session.jabberID, msn2jid(contact), False)
311 del self.contactTyping[contact]
312
313 def gotContactTyping(self, contact):
314 if not self.session: return
315 # Check if the contact has only just started typing
316 if not self.contactTyping.has_key(contact):
317 self.session.sendTypingNotification(self.session.jabberID, msn2jid(contact), True)
318
319 # Reset the counter
320 self.contactTyping[contact] = 0
321
322 def userTypingNotification(self, dest, resource, composing):
323 if not self.session: return
324 dest = jid2msn(dest)
325 self.userTyping[dest] = composing
326 if composing: # Make it instant
327 self.sendTypingToContact(dest)
328
329 def listSynchronized(self):
330 if not self.session: return
331 self.session.sendPresence(to=self.session.jabberID, fro=config.jid)
332 self.legacyList.syncJabberLegacyLists()
333 self.listSynced = True
334 #self.legacyList.flushSubscriptionBuffer()
335
336 def gotMessage(self, remoteUser, text):
337 if not self.session: return
338 source = msn2jid(remoteUser)
339 self.session.sendMessage(self.session.jabberID, fro=source, body=text, mtype="chat")
340 self.session.pytrans.statistics.stats["MessageCount"] += 1
341
342 def avatarHashChanged(self, userHandle, hash):
343 if not self.session: return
344
345 if not hash:
346 # They've turned off their avatar
347 c = self.session.contactList.findContact(jid)
348 if not c: return
349 c.updateAvatar(av)
350 else:
351 # New avatar
352 av = self.session.pytrans.avatarCache.getAvatar(hash)
353 if av:
354 msnContact = self.getContacts().getContact(userHandle)
355 msnContact.msnobjGot = True
356 jid = msn2jid(userHandle)
357 c = self.session.contactList.findContact(jid)
358 if not c: return
359 c.updateAvatar(av)
360 else:
361 self.requestAvatar(userHandle)
362
363 def gotAvatarImage(self, userHandle, imageData):
364 if not self.session: return
365 jid = msn2jid(userHandle)
366 c = self.session.contactList.findContact(jid)
367 if not c: return
368 av = self.session.pytrans.avatarCache.setAvatar(imageData)
369 c.updateAvatar(av)
370
371 def gotSendRequest(self, fileReceive):
372 if not self.session: return
373 LogEvent(INFO, self.session.jabberID)
374 ft.FTReceive(self.session, msn2jid(fileReceive.userHandle), fileReceive)
375
376 def loggedIn(self):
377 if not self.session: return
378 LogEvent(INFO, self.session.jabberID)
379 self.session.ready = True
380
381 def contactStatusChanged(self, remoteUser, statusCode, screenName):
382 if not (self.session and self.getContacts()): return
383 LogEvent(INFO, self.session.jabberID)
384
385 msnContact = self.getContacts().getContact(remoteUser)
386 c = self.session.contactList.findContact(msn2jid(remoteUser))
387 if not (c and msnContact): return
388
389 show, ptype = state2presence(msnContact.status)
390 status = msnContact.personal.decode("utf-8")
391 screenName = msnContact.screenName.decode("utf-8")
392
393 c.updateNickname(screenName, push=False)
394 c.updatePresence(show, status, ptype, force=True)
395
396 def ourStatusChanged(self, statusCode):
397 # Send out a new presence packet to the Jabber user so that the MSN-t icon changes
398 if not self.session: return
399 LogEvent(INFO, self.session.jabberID)
400 self.remoteShow, ptype = state2presence(statusCode)
401 self.sendShowStatus()
402
403 def ourPersonalChanged(self, statusMessage):
404 if not self.session: return
405 LogEvent(INFO, self.session.jabberID)
406 self.remoteStatus = statusMessage
407 self.sendShowStatus()
408
409 def ourNickChanged(self, nick):
410 if not self.session: return
411 LogEvent(INFO, self.session.jabberID)
412 self.remoteNick = nick
413 self.sendShowStatus()
414
415 def sendShowStatus(self):
416 if not self.session: return
417 source = config.jid
418 to = self.session.jabberID
419 self.session.sendPresence(to=to, fro=source, show=self.remoteShow, status=self.remoteStatus, nickname=self.remoteNick)
420
421 def userMapping(self, passport, jid):
422 if not self.session: return
423 text = lang.get(self.session.lang).userMapping % (passport, jid)
424 self.session.sendMessage(to=self.session.jabberID, fro=msn2jid(passport), body=text)
425
426 def userAddedMe(self, userHandle):
427 if not self.session: return
428 self.session.contactList.getContact(msn2jid(userHandle)).contactRequestsAuth()
429
430 def userRemovedMe(self, userHandle):
431 if not self.session: return
432 c = self.session.contactList.getContact(msn2jid(userHandle))
433 c.contactDerequestsAuth()
434 c.contactRemovesAuth()
435
436 def serverGoingDown(self):
437 if not self.session: return
438 self.session.sendMessage(to=self.session.jabberID, fro=config.jid, body=lang.get(self.session.lang).msnMaintenance)
439
440 def multipleLogin(self):
441 if not self.session: return
442 self.session.sendMessage(to=self.session.jabberID, fro=config.jid, body=lang.get(self.session.lang).msnMultipleLogin)
443 self.session.removeMe()
444
445 def accountNotVerified(self):
446 if not self.session: return
447 text = lang.get(self.session.lang).msnNotVerified % (self.session.username)
448 self.session.sendMessage(to=self.session.jabberID, fro=config.jid, body=text)
449
450 def loginFailure(self, message):
451 if not self.session: return
452 text = lang.get(self.session.lang).msnLoginFailure % (self.session.username)
453 self.session.sendErrorMessage(to=self.session.jabberID, fro=config.jid, etype="auth", condition="not-authorized", explanation=text, body="Login Failure")
454 self.session.removeMe()
455
456 def failedMessage(self, remoteUser, message):
457 if not self.session: return
458 self.session.pytrans.statistics.stats["FailedMessageCount"] += 1
459 fro = msn2jid(remoteUser)
460 self.session.sendErrorMessage(to=self.session.jabberID, fro=fro, etype="wait", condition="recipient-unavailable", explanation=lang.get(self.session.lang).msnFailedMessage, body=message)
461
462 def initialEmailNotification(self, inboxunread, foldersunread):
463 if not self.session: return
464 text = lang.get(self.session.lang).msnInitialMail % (inboxunread, foldersunread)
465 self.session.sendMessage(to=self.session.jabberID, fro=config.jid, body=text, mtype="headline")
466
467 def realtimeEmailNotification(self, mailfrom, fromaddr, subject):
468 if not self.session: return
469 text = lang.get(self.session.lang).msnRealtimeMail % (mailfrom, fromaddr, subject)
470 self.session.sendMessage(to=self.session.jabberID, fro=config.jid, body=text, mtype="headline")
471
472 def connectionLost(self, reason):
473 if not self.session: return
474 LogEvent(INFO, self.session.jabberID)
475 text = lang.get(self.session.lang).msnDisconnected % ("Error") # FIXME, a better error would be nice =P
476 self.session.sendMessage(to=self.session.jabberID, fro=config.jid, body=text)
477 self.session.removeMe() # Tear down the session
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