]> code.delx.au - pymsnt/blob - src/legacy/glue.py
Trunk is broken. Don't use!
[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, msnp2p
8 from debug import LogEvent, INFO, WARN, ERROR
9 import sha
10 import groupchat
11 import ft
12 import avatar
13 import msnw
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 avatar
28 f = open("src/legacy/defaultJabberAvatar.png")
29 defaultJabberAvatarData = f.read()
30 f.close()
31
32
33 def isGroupJID(jid):
34 """ Returns True if the JID passed is a valid groupchat JID (for MSN, does not contain '%') """
35 return (jid.find('%') == -1)
36
37
38
39 # This should be set to the name space the registration entries are in, in the xdb spool
40 namespace = "jabber:iq:register"
41
42
43 def formRegEntry(username, password):
44 """ Returns a domish.Element representation of the data passed. This element will be written to the XDB spool file """
45 reginfo = Element((None, "query"))
46 reginfo.attributes["xmlns"] = "jabber:iq:register"
47
48 userEl = reginfo.addElement("username")
49 userEl.addContent(username)
50
51 passEl = reginfo.addElement("password")
52 passEl.addContent(password)
53
54 return reginfo
55
56
57
58
59 def getAttributes(base):
60 """ This function should, given a spool domish.Element, pull the username, password,
61 and out of it and return them """
62 username = ""
63 password = ""
64 for child in base.elements():
65 try:
66 if child.name == "username":
67 username = child.__str__()
68 elif child.name == "password":
69 password = child.__str__()
70 except AttributeError:
71 continue
72
73 return username, password
74
75
76 def startStats(statistics):
77 stats = statistics.stats
78 stats["MessageCount"] = 0
79 stats["FailedMessageCount"] = 0
80 stats["AvatarCount"] = 0
81 stats["FailedAvatarCount"] = 0
82
83 def updateStats(statistics):
84 stats = statistics.stats
85 stats["AvatarCount"] = msnp2p.MSNP2P_Avatar.TRANSFER_COUNT
86 stats["FailedAvatarCount"] = msnp2p.MSNP2P_Avatar.ERROR_COUNT
87
88
89 def msn2jid(msnid):
90 """ Converts a MSN passport into a JID representation to be used with the transport """
91 return msnid.replace('@', '%') + "@" + config.jid
92
93 translateAccount = msn2jid # Marks this as the function to be used in jabber:iq:gateway (Service ID Translation)
94
95 def jid2msn(jid):
96 """ Converts a JID representation of a MSN passport into the original MSN passport """
97 return unicode(jid[:jid.find('@')].replace('%', '@'))
98
99
100 def presence2state(show, ptype):
101 """ Converts a Jabber presence into an MSN status code """
102 if ptype == "unavailable":
103 return msn.STATUS_OFFLINE
104 elif not show or show == "online" or show == "chat":
105 return msn.STATUS_ONLINE
106 elif show == "dnd":
107 return msn.STATUS_BUSY
108 elif show == "away" or show == "xa":
109 return msn.STATUS_AWAY
110
111
112 def state2presence(state):
113 """ Converts a MSN status code into a Jabber presence """
114 if state == msn.STATUS_ONLINE:
115 return (None, None)
116 elif state == msn.STATUS_BUSY:
117 return ("dnd", None)
118 elif state == msn.STATUS_AWAY:
119 return ("away", None)
120 elif state == msn.STATUS_IDLE:
121 return ("away", None)
122 elif state == msn.STATUS_BRB:
123 return ("away", None)
124 elif state == msn.STATUS_PHONE:
125 return ("dnd", None)
126 elif state == msn.STATUS_LUNCH:
127 return ("away", None)
128 else:
129 return (None, "unavailable")
130
131
132
133
134 # This class handles groupchats with the legacy protocol
135 class LegacyGroupchat(groupchat.BaseGroupchat):
136 def __init__(self, session, resource, ID=None, existing=False, switchboardSession=None):
137 """ Possible entry points for groupchat
138 - User starts an empty switchboard session by sending presence to a blank room
139 - An existing switchboard session is joined by another MSN user
140 - User invited to an existing switchboard session with more than one user
141 """
142 groupchat.BaseGroupchat.__init__(self, session, resource, ID)
143 if not existing:
144 self.switchboardSession = msnw.GroupchatSwitchboardSession(self, makeSwitchboard=True)
145 else:
146 self.switchboardSession = switchboardSession
147
148 assert(self.switchboardSession != None)
149
150 LogEvent(INFO, self.roomJID())
151
152 def removeMe(self):
153 self.switchboardSession.removeMe()
154 self.switchboardSession = None
155 groupchat.BaseGroupchat.removeMe(self)
156 LogEvent(INFO, self.roomJID())
157
158 def sendLegacyMessage(self, message, noerror):
159 LogEvent(INFO, self.roomJID())
160 self.switchboardSession.sendMessage(message.replace("\n", "\r\n"), noerror)
161
162 def sendContactInvite(self, contactJID):
163 LogEvent(INFO, self.roomJID())
164 userHandle = jid2msn(contactJID)
165 self.switchboardSession.inviteUser(userHandle)
166
167
168
169 # This class handles most interaction with the legacy protocol
170 class LegacyConnection(msnw.MSNConnection):
171 """ A glue class that connects to the legacy network """
172 def __init__(self, username, password, session):
173 self.session = session
174 self.listSynced = False
175 self.initialListVersion = 0
176
177 self.remoteShow = ""
178 self.remoteStatus = ""
179 self.remoteNick = ""
180
181 # Init the MSN bits
182 msnw.MSNConnection.__init__(self, username, password)
183
184 # User typing notification stuff
185 self.userTyping = dict() # Indexed by contact MSN ID, stores whether the user is typing to this contact
186 # Contact typing notification stuff
187 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
188 # Looping function
189 self.userTypingSend = task.LoopingCall(self.sendTypingNotifications)
190 self.userTypingSend.start(5.0)
191
192 import legacylist # Is in here to prevent an ImportError loop
193 self.legacyList = legacylist.LegacyList(self.session)
194
195 LogEvent(INFO, self.session.jabberID)
196
197 def removeMe(self):
198 LogEvent(INFO, self.session.jabberID)
199
200 self.userTypingSend.stop()
201
202 msnw.MSNConnection.removeMe(self)
203 self.legacyList.removeMe()
204 self.legacyList = None
205 self.session = None
206
207 def jidRes(self, resource):
208 to = self.session.jabberID
209 if resource:
210 to += "/" + resource
211
212 return to
213
214 def highestResource(self):
215 """ Returns highest priority resource """
216 return self.session.highestResource()
217
218 def sendMessage(self, dest, resource, body, noerror):
219 dest = jid2msn(dest)
220 if self.userTyping.has_key(dest):
221 del self.userTyping[dest]
222 try:
223 msnw.MSNConnection.sendMessage(self, dest, resource, body, noerror)
224 self.session.pytrans.statistics.stats["MessageCount"] += 1
225 except:
226 self.failedMessage(dest, body)
227 raise
228
229 def msnAlert(self, text, actionurl, subscrurl):
230 if not self.session: return
231
232 el = Element((None, "message"))
233 el.attributes["to"] = self.session.jabberID
234 el.attributes["from"] = config.jid
235 el.attributes["type"] = "headline"
236 body = el.addElement("body")
237 body.addContent(text)
238
239 x = el.addElement("x")
240 x.attributes["xmlns"] = "jabber:x:oob"
241 x.addElement("desc").addContent("More information on this notice.")
242 x.addElement("url").addContent(actionurl)
243
244 x = el.addElement("x")
245 x.attributes["xmlns"] = "jabber:x:oob"
246 x.addElement("desc").addContent("Manage subscriptions to alerts.")
247 x.addElement("url").addContent(subscrurl)
248
249 self.session.pytrans.send(el)
250
251 def setStatus(self, nickname, show, status):
252 statusCode = presence2state(show, None)
253 msnw.MSNConnection.changeStatus(self, statusCode, nickname, status)
254
255 def updateAvatar(self, av=None):
256 global defaultJabberAvatarData
257
258 if av:
259 msnw.MSNConnection.changeAvatar(self, av.getImageData())
260 else:
261 msnw.MSNConnection.changeAvatar(self, defaultJabberAvatarData)
262
263 def sendTypingNotifications(self):
264 if not self.session: return
265
266 # Send any typing notification messages to the user's contacts
267 for contact in self.userTyping.keys():
268 if self.userTyping[contact]:
269 self.sendTypingToContact(contact)
270
271 # Send any typing notification messages from contacts to the user
272 for contact, resource in self.contactTyping.keys():
273 self.contactTyping[(contact, resource)] += 1
274 if self.contactTyping[(contact, resource)] >= 3:
275 self.session.sendTypingNotification(self.jidRes(resource), msn2jid(contact), False)
276 del self.contactTyping[(contact, resource)]
277
278 def gotContactTyping(self, contact, resource):
279 if not self.session: return
280 # Check if the contact has only just started typing
281 if not self.contactTyping.has_key((contact, resource)):
282 self.session.sendTypingNotification(self.jidRes(resource), msn2jid(contact), True)
283
284 # Reset the counter
285 self.contactTyping[(contact, resource)] = 0
286
287 def userTypingNotification(self, dest, resource, composing):
288 if not self.session: return
289 dest = jid2msn(dest)
290 self.userTyping[dest] = composing
291 if composing: # Make it instant
292 self.sendTypingToContact(dest)
293
294 def listSynchronized(self):
295 if not self.session: return
296 self.session.sendPresence(to=self.session.jabberID, fro=config.jid)
297 self.legacyList.syncJabberLegacyLists()
298 self.listSynced = True
299 #self.legacyList.flushSubscriptionBuffer()
300
301 def gotMessage(self, remoteUser, resource, text):
302 if not self.session: return
303 source = msn2jid(remoteUser)
304 self.session.sendMessage(self.jidRes(resource), fro=source, body=text, mtype="chat")
305 self.session.pytrans.statistics.stats["MessageCount"] += 1
306
307 def avatarHashChanged(self, userHandle, hash):
308 if not self.session: return
309
310 if not hash:
311 # They've turned off their avatar
312 c = self.session.contactList.findContact(jid)
313 if not c: return
314 c.updateAvatar(av)
315 else:
316 # New avatar
317 av = self.session.pytrans.avatarCache.getAvatar(hash)
318 if av:
319 msnContact = self.getContacts().getContact(userHandle)
320 msnContact.msnobjGot = True
321 jid = msn2jid(userHandle)
322 c = self.session.contactList.findContact(jid)
323 if not c: return
324 c.updateAvatar(av)
325 else:
326 self.requestAvatar(userHandle)
327
328 def gotAvatarImage(self, userHandle, imageData):
329 if not self.session: return
330 jid = msn2jid(userHandle)
331 c = self.session.contactList.findContact(jid)
332 if not c: return
333 av = self.session.pytrans.avatarCache.setAvatar(imageData)
334 c.updateAvatar(av)
335
336 def gotSendRequest(self, fileReceive):
337 if not self.session: return
338 LogEvent(INFO, self.session.jabberID)
339 ft.FTReceive(self.session, msn2jid(fileReceive.userHandle), fileReceive)
340
341 def loggedIn(self):
342 if not self.session: return
343 LogEvent(INFO, self.session.jabberID)
344 self.session.ready = True
345
346 def contactStatusChanged(self, remoteUser):
347 if not (self.session and self.getContacts()): return
348 LogEvent(INFO, self.session.jabberID)
349
350 msnContact = self.getContacts().getContact(remoteUser)
351 c = self.session.contactList.findContact(msn2jid(remoteUser))
352 if not (c and msnContact): return
353
354 show, ptype = state2presence(msnContact.status)
355 status = msnContact.personal.decode("utf-8")
356 screenName = msnContact.screenName.decode("utf-8")
357
358 c.updateNickname(screenName, push=False)
359 c.updatePresence(show, status, ptype, force=True)
360
361 def ourStatusChanged(self, statusCode):
362 # Send out a new presence packet to the Jabber user so that the MSN-t icon changes
363 if not self.session: return
364 LogEvent(INFO, self.session.jabberID)
365 self.remoteShow, ptype = state2presence(statusCode)
366 self.sendShowStatus()
367
368 def ourPersonalChanged(self, statusMessage):
369 if not self.session: return
370 LogEvent(INFO, self.session.jabberID)
371 self.remoteStatus = statusMessage
372 self.sendShowStatus()
373
374 def ourNickChanged(self, nick):
375 if not self.session: return
376 LogEvent(INFO, self.session.jabberID)
377 self.remoteNick = nick
378 self.sendShowStatus()
379
380 def sendShowStatus(self):
381 if not self.session: return
382 source = config.jid
383 to = self.session.jabberID
384 self.session.sendPresence(to=to, fro=source, show=self.remoteShow, status=self.remoteStatus, nickname=self.remoteNick)
385
386 def userMapping(self, passport, jid):
387 if not self.session: return
388 text = lang.get(self.session.lang).userMapping % (passport, jid)
389 self.session.sendMessage(to=self.session.jabberID, fro=msn2jid(passport), body=text)
390
391 def userAddedMe(self, userHandle):
392 if not self.session: return
393 self.session.contactList.getContact(msn2jid(userHandle)).contactRequestsAuth()
394
395 def userRemovedMe(self, userHandle):
396 if not self.session: return
397 c = self.session.contactList.getContact(msn2jid(userHandle))
398 c.contactDerequestsAuth()
399 c.contactRemovesAuth()
400
401 def serverGoingDown(self):
402 if not self.session: return
403 self.session.sendMessage(to=self.session.jabberID, fro=config.jid, body=lang.get(self.session.lang).msnMaintenance)
404
405 def multipleLogin(self):
406 if not self.session: return
407 self.session.sendMessage(to=self.session.jabberID, fro=config.jid, body=lang.get(self.session.lang).msnMultipleLogin)
408 self.session.removeMe()
409
410 def accountNotVerified(self):
411 if not self.session: return
412 text = lang.get(self.session.lang).msnNotVerified % (self.session.username)
413 self.session.sendMessage(to=self.session.jabberID, fro=config.jid, body=text)
414
415 def loginFailure(self, message):
416 if not self.session: return
417 text = lang.get(self.session.lang).msnLoginFailure % (self.session.username)
418 self.session.sendErrorMessage(to=self.session.jabberID, fro=config.jid, etype="auth", condition="not-authorized", explanation=text, body="Login Failure")
419 self.session.removeMe()
420
421 def failedMessage(self, remoteUser, message):
422 if not self.session: return
423 self.session.pytrans.statistics.stats["FailedMessageCount"] += 1
424 fro = msn2jid(remoteUser)
425 self.session.sendErrorMessage(to=self.session.jabberID, fro=fro, etype="wait", condition="recipient-unavailable", explanation=lang.get(self.session.lang).msnFailedMessage, body=message)
426
427 def initialEmailNotification(self, inboxunread, foldersunread):
428 if not self.session: return
429 text = lang.get(self.session.lang).msnInitialMail % (inboxunread, foldersunread)
430 self.session.sendMessage(to=self.session.jabberID, fro=config.jid, body=text, mtype="headline")
431
432 def realtimeEmailNotification(self, mailfrom, fromaddr, subject):
433 if not self.session: return
434 text = lang.get(self.session.lang).msnRealtimeMail % (mailfrom, fromaddr, subject)
435 self.session.sendMessage(to=self.session.jabberID, fro=config.jid, body=text, mtype="headline")
436
437 def connectionLost(self, reason):
438 if not self.session: return
439 LogEvent(INFO, self.jabberID)
440 text = lang.get(self.session.lang).msnDisconnected % ("Error") # FIXME, a better error would be nice =P
441 self.session.sendMessage(to=self.session.jabberID, fro=config.jid, body=text)
442 self.session.removeMe() # Tear down the session
443
444