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