]> code.delx.au - pymsnt/blob - src/legacy/glue.py
Integrating new msnw more. Groupchat. About to test..
[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)
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 c = self.session.contactList.findContact(jid)
395 if not c: return
396
397 if hash:
398 # New avatar
399 av = self.session.pytrans.avatarCache.getAvatar(hash)
400 if av:
401 msnContact = self.getContacts().getContact(userHandle)
402 msnContact.msnobjGot = True
403 jid = msn2jid(userHandle)
404 c.updateAvatar(av)
405 else:
406 def updateAvatar((imageData,)):
407 av = self.session.pytrans.avatarCache.setAvatar(imageData)
408 c.updateAvatar(av)
409 self.sendAvatarRequest(userHandle).addCallback(updateAvatar)
410 else:
411 # They've turned off their avatar
412 global defaultAvatar
413 c.updateAvatar(defaultAvatar)
414
415 def contactStatusChanged(self, remoteUser):
416 LogEvent(INFO, self.session.jabberID)
417
418 msnContact = self.getContacts().getContact(remoteUser)
419 c = self.session.contactList.findContact(msn2jid(remoteUser))
420 if not (c and msnContact): return
421
422 show, ptype = state2presence(msnContact.status)
423 status = msnContact.personal.decode("utf-8")
424 screenName = msnContact.screenName.decode("utf-8")
425
426 c.updateNickname(screenName, push=False)
427 c.updatePresence(show, status, ptype, force=True)
428
429 def gotFileReceive(self, fileReceive):
430 LogEvent(INFO, self.session.jabberID)
431 pass # FIXME
432
433 def contactAddedMe(self, userHandle):
434 LogEvent(INFO, self.session.jabberID)
435 self.session.contactList.getContact(msn2jid(userHandle)).contactRequestsAuth()
436
437 def contactRemovedMe(self, userHandle):
438 LogEvent(INFO, self.session.jabberID)
439 c = self.session.contactList.getContact(msn2jid(userHandle))
440 c.contactDerequestsAuth()
441 c.contactRemovesAuth()
442
443 def gotInitialEmailNotification(self, inboxunread, foldersunread):
444 LogEvent(INFO, self.session.jabberID)
445 text = lang.get(self.session.lang).msnInitialMail % (inboxunread, foldersunread)
446 self.session.sendMessage(to=self.session.jabberID, fro=config.jid, body=text, mtype="headline")
447
448 def gotRealtimeEmailNotification(self, mailfrom, fromaddr, subject):
449 LogEvent(INFO, self.session.jabberID)
450 text = lang.get(self.session.lang).msnRealtimeMail % (mailfrom, fromaddr, subject)
451 self.session.sendMessage(to=self.session.jabberID, fro=config.jid, body=text, mtype="headline")
452
453 def gotMSNAlert(self, text, actionurl, subscrurl):
454 LogEvent(INFO, self.session.jabberID)
455
456 el = Element((None, "message"))
457 el.attributes["to"] = self.session.jabberID
458 el.attributes["from"] = config.jid
459 el.attributes["type"] = "headline"
460 body = el.addElement("body")
461 body.addContent(text)
462
463 x = el.addElement("x")
464 x.attributes["xmlns"] = "jabber:x:oob"
465 x.addElement("desc").addContent("More information on this notice.")
466 x.addElement("url").addContent(actionurl)
467
468 x = el.addElement("x")
469 x.attributes["xmlns"] = "jabber:x:oob"
470 x.addElement("desc").addContent("Manage subscriptions to alerts.")
471 x.addElement("url").addContent(subscrurl)
472
473 self.session.pytrans.send(el)
474
475
476
477
478 class LegacyList:
479 def __init__(self, session):
480 self.session = session
481 self.subscriptionBuffer = []
482
483 def removeMe(self):
484 self.subscriptionBuffer = None
485 self.session = None
486
487 def addContact(self, jid):
488 LogEvent(INFO, self.session.jabberID)
489 userHandle = jid2msn(jid)
490 self.session.legacycon.addContact(msn.FORWARD_LIST, userHandle)
491 self.session.contactList.getContact(jid).contactGrantsAuth()
492
493 def removeContact(self, jid):
494 LogEvent(INFO, self.session.jabberID)
495 jid = jid2msn(jid)
496 self.session.legacycon.remContact(msn.FORWARD_LIST, jid)
497
498
499 def authContact(self, jid):
500 LogEvent(INFO, self.session.jabberID)
501 jid = jid2msn(jid)
502 d = self.session.legacycon.remContact(msn.PENDING_LIST, jid)
503 if d:
504 self.session.legacycon.addContact(msn.REVERSE_LIST, jid)
505 self.session.legacycon.remContact(msn.BLOCK_LIST, jid)
506 self.session.legacycon.addContact(msn.ALLOW_LIST, jid)
507
508 def deauthContact(self, jid):
509 LogEvent(INFO, self.session.jabberID)
510 jid = jid2msn(jid)
511 self.session.legacycon.remContact(msn.ALLOW_LIST, jid)
512 self.session.legacycon.addContact(msn.BLOCK_LIST, jid)
513
514
515
516 def syncJabberLegacyLists(self):
517 """ Synchronises the MSN contact list on server with the Jabber contact list """
518
519 global defaultAvatar
520
521 # We have to make an MSNContactList from the XDB data, then compare it with the one the server sent
522 # Any subscription changes must be sent to the client, as well as changed in the XDB
523 LogEvent(INFO, self.session.jabberID, "Start.")
524 result = self.session.pytrans.xdb.request(self.session.jabberID, disco.IQROSTER)
525 oldContactList = msn.MSNContactList()
526 if result:
527 for item in result.elements():
528 user = item.getAttribute("jid")
529 sub = item.getAttribute("subscription")
530 lists = item.getAttribute("lists")
531 if not lists:
532 lists = jabsub2msnlist(sub) # Backwards compatible
533 lists = int(lists)
534 contact = msn.MSNContact(userHandle=user, screenName="", lists=lists)
535 oldContactList.addContact(contact)
536
537 newXDB = Element((None, "query"))
538 newXDB.attributes["xmlns"] = disco.IQROSTER
539
540 contactList = self.session.legacycon.getContacts()
541
542
543 # Convienence functions
544 def addedToList(num):
545 return (not (oldLists & num) and (lists & num))
546 def removedFromList(num):
547 return ((oldLists & num) and not (lists & num))
548
549 for contact in contactList.contacts.values():
550 # Compare with the XDB <item/> entry
551 oldContact = oldContactList.getContact(contact.userHandle)
552 if oldContact == None:
553 oldLists = 0
554 else:
555 oldLists = oldContact.lists
556 lists = contact.lists
557
558 # Create the Jabber representation of the
559 # contact base on the old list data and then
560 # sync it with current
561 jabContact = self.session.contactList.createContact(msn2jid(contact.userHandle), msnlist2jabsub(oldLists))
562 jabContact.updateAvatar(defaultAvatar, push=False)
563
564 if addedToList(msn.FORWARD_LIST):
565 jabContact.syncGroups(getGroupNames(contact, contactList), push=False)
566 jabContact.syncContactGrantedAuth()
567
568 if removedFromList(msn.FORWARD_LIST):
569 jabContact.syncContactRemovedAuth()
570
571 if addedToList(msn.ALLOW_LIST):
572 jabContact.syncUserGrantedAuth()
573
574 if addedToList(msn.BLOCK_LIST) or removedFromList(msn.ALLOW_LIST):
575 jabContact.syncUserRemovedAuth()
576
577 if (not (lists & msn.ALLOW_LIST) and not (lists & msn.BLOCK_LIST) and (lists & msn.REVERSE_LIST)) or (lists & msn.PENDING_LIST):
578 jabContact.contactRequestsAuth()
579
580 if removedFromList(msn.REVERSE_LIST):
581 jabContact.contactDerequestsAuth()
582
583 item = newXDB.addElement("item")
584 item.attributes["jid"] = contact.userHandle
585 item.attributes["subscription"] = msnlist2jabsub(lists)
586 item.attributes["lists"] = str(lists)
587
588 # Update the XDB
589 self.session.pytrans.xdb.set(self.session.jabberID, disco.IQROSTER, newXDB)
590 LogEvent(INFO, self.session.jabberID, "End.")
591
592 def saveLegacyList(self):
593 contactList = self.session.legacycon.getContacts()
594 if not contactList: return
595
596 newXDB = Element((None, "query"))
597 newXDB.attributes["xmlns"] = disco.IQROSTER
598
599 for contact in contactList.contacts.values():
600 item = newXDB.addElement("item")
601 item.attributes["jid"] = contact.userHandle
602 item.attributes["subscription"] = msnlist2jabsub(contact.lists) # Backwards compat
603 item.attributes["lists"] = str(contact.lists)
604
605 self.session.pytrans.xdb.set(self.session.jabberID, disco.IQROSTER, newXDB)
606 LogEvent(INFO, self.session.jabberID, "Finished saving list.")
607
608