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