8a97f4ee0c3fd9b3e9c96c5638236aa2f2c7fa23
[gc-dialer] / src / session.py
1 from __future__ import with_statement
2
3 import os
4 import time
5 import datetime
6 import contextlib
7 import logging
8
9 try:
10         import cPickle
11         pickle = cPickle
12 except ImportError:
13         import pickle
14
15 from PyQt4 import QtCore
16
17 from util import qore_utils
18 from util import qui_utils
19 from util import concurrent
20 from util import misc as misc_utils
21
22 import constants
23
24
25 _moduleLogger = logging.getLogger(__name__)
26
27
28 class _DraftContact(object):
29
30         def __init__(self, messageId, title, description, numbersWithDescriptions):
31                 self.messageId = messageId
32                 self.title = title
33                 self.description = description
34                 self.numbers = numbersWithDescriptions
35                 self.selectedNumber = numbersWithDescriptions[0][0]
36
37
38 class Draft(QtCore.QObject):
39
40         sendingMessage = QtCore.pyqtSignal()
41         sentMessage = QtCore.pyqtSignal()
42         calling = QtCore.pyqtSignal()
43         called = QtCore.pyqtSignal()
44         cancelling = QtCore.pyqtSignal()
45         cancelled = QtCore.pyqtSignal()
46         error = QtCore.pyqtSignal(str)
47
48         recipientsChanged = QtCore.pyqtSignal()
49
50         def __init__(self, pool, backend, errorLog):
51                 QtCore.QObject.__init__(self)
52                 self._errorLog = errorLog
53                 self._contacts = {}
54                 self._pool = pool
55                 self._backend = backend
56                 self._busyReason = None
57                 self._message = ""
58
59         def send(self):
60                 assert 0 < len(self._contacts), "No contacts selected"
61                 assert 0 < len(self._message), "No message to send"
62                 numbers = [misc_utils.make_ugly(contact.selectedNumber) for contact in self._contacts.itervalues()]
63                 le = concurrent.AsyncLinearExecution(self._pool, self._send)
64                 le.start(numbers, self._message)
65
66         def call(self):
67                 assert len(self._contacts) == 1, "Must select 1 and only 1 contact"
68                 assert len(self._message) == 0, "Cannot send message with call"
69                 (contact, ) = self._contacts.itervalues()
70                 number = misc_utils.make_ugly(contact.selectedNumber)
71                 le = concurrent.AsyncLinearExecution(self._pool, self._call)
72                 le.start(number)
73
74         def cancel(self):
75                 le = concurrent.AsyncLinearExecution(self._pool, self._cancel)
76                 le.start()
77
78         def _get_message(self):
79                 return self._message
80
81         def _set_message(self, message):
82                 self._message = message
83
84         message = property(_get_message, _set_message)
85
86         def add_contact(self, contactId, messageId, title, description, numbersWithDescriptions):
87                 if self._busyReason is not None:
88                         raise RuntimeError("Please wait for %r" % self._busyReason)
89                 # Allow overwriting of contacts so that the message can be updated and the SMS dialog popped back up
90                 contactDetails = _DraftContact(messageId, title, description, numbersWithDescriptions)
91                 self._contacts[contactId] = contactDetails
92                 self.recipientsChanged.emit()
93
94         def remove_contact(self, contactId):
95                 if self._busyReason is not None:
96                         raise RuntimeError("Please wait for %r" % self._busyReason)
97                 assert contactId in self._contacts, "Contact missing"
98                 del self._contacts[contactId]
99                 self.recipientsChanged.emit()
100
101         def get_contacts(self):
102                 return self._contacts.iterkeys()
103
104         def get_num_contacts(self):
105                 return len(self._contacts)
106
107         def get_message_id(self, cid):
108                 return self._contacts[cid].messageId
109
110         def get_title(self, cid):
111                 return self._contacts[cid].title
112
113         def get_description(self, cid):
114                 return self._contacts[cid].description
115
116         def get_numbers(self, cid):
117                 return self._contacts[cid].numbers
118
119         def get_selected_number(self, cid):
120                 return self._contacts[cid].selectedNumber
121
122         def set_selected_number(self, cid, number):
123                 # @note I'm lazy, this isn't firing any kind of signal since only one
124                 # controller right now and that is the viewer
125                 assert number in (nWD[0] for nWD in self._contacts[cid].numbers), "Number not selectable"
126                 self._contacts[cid].selectedNumber = number
127
128         def clear(self):
129                 if self._busyReason is not None:
130                         raise RuntimeError("Please wait for %r" % self._busyReason)
131                 self._clear()
132
133         def _clear(self):
134                 oldContacts = self._contacts
135                 self._contacts = {}
136                 self._message = ""
137                 if oldContacts:
138                         self.recipientsChanged.emit()
139
140         @contextlib.contextmanager
141         def _busy(self, message):
142                 if self._busyReason is not None:
143                         raise RuntimeError("Already busy doing %r" % self._busyReason)
144                 try:
145                         self._busyReason = message
146                         yield
147                 finally:
148                         self._busyReason = None
149
150         def _send(self, numbers, text):
151                 self.sendingMessage.emit()
152                 try:
153                         with self._busy("Sending Text"):
154                                 with qui_utils.notify_busy(self._errorLog, "Sending Text"):
155                                         yield (
156                                                 self._backend[0].send_sms,
157                                                 (numbers, text),
158                                                 {},
159                                         )
160                                 self.sentMessage.emit()
161                                 self._clear()
162                 except Exception, e:
163                         _moduleLogger.exception("Reporting error to user")
164                         self.error.emit(str(e))
165
166         def _call(self, number):
167                 self.calling.emit()
168                 try:
169                         with self._busy("Calling"):
170                                 with qui_utils.notify_busy(self._errorLog, "Calling"):
171                                         yield (
172                                                 self._backend[0].call,
173                                                 (number, ),
174                                                 {},
175                                         )
176                                 self.called.emit()
177                                 self._clear()
178                 except Exception, e:
179                         _moduleLogger.exception("Reporting error to user")
180                         self.error.emit(str(e))
181
182         def _cancel(self):
183                 self.cancelling.emit()
184                 try:
185                         with qui_utils.notify_busy(self._errorLog, "Cancelling"):
186                                 yield (
187                                         self._backend[0].cancel,
188                                         (),
189                                         {},
190                                 )
191                         self.cancelled.emit()
192                 except Exception, e:
193                         _moduleLogger.exception("Reporting error to user")
194                         self.error.emit(str(e))
195
196
197 class Session(QtCore.QObject):
198
199         # @todo Somehow add support for csv contacts
200
201         stateChange = QtCore.pyqtSignal(str)
202         loggedOut = QtCore.pyqtSignal()
203         loggedIn = QtCore.pyqtSignal()
204         callbackNumberChanged = QtCore.pyqtSignal(str)
205
206         accountUpdated = QtCore.pyqtSignal()
207         messagesUpdated = QtCore.pyqtSignal()
208         newMessages = QtCore.pyqtSignal()
209         historyUpdated = QtCore.pyqtSignal()
210         dndStateChange = QtCore.pyqtSignal(bool)
211         voicemailAvailable = QtCore.pyqtSignal(str, str)
212
213         error = QtCore.pyqtSignal(str)
214
215         LOGGEDOUT_STATE = "logged out"
216         LOGGINGIN_STATE = "logging in"
217         LOGGEDIN_STATE = "logged in"
218
219         MESSAGE_TEXTS = "Text"
220         MESSAGE_VOICEMAILS = "Voicemail"
221         MESSAGE_ALL = "All"
222
223         _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.3.0")
224
225         _LOGGEDOUT_TIME = -1
226         _LOGGINGIN_TIME = 0
227
228         def __init__(self, errorLog, cachePath = None):
229                 QtCore.QObject.__init__(self)
230                 self._errorLog = errorLog
231                 self._pool = qore_utils.AsyncPool()
232                 self._backend = []
233                 self._loggedInTime = self._LOGGEDOUT_TIME
234                 self._loginOps = []
235                 self._cachePath = cachePath
236                 self._voicemailCachePath = None
237                 self._username = None
238                 self._password = None
239                 self._draft = Draft(self._pool, self._backend, self._errorLog)
240                 self._delayedRelogin = QtCore.QTimer()
241                 self._delayedRelogin.setInterval(0)
242                 self._delayedRelogin.setSingleShot(True)
243                 self._delayedRelogin.timeout.connect(self._on_delayed_relogin)
244
245                 self._contacts = {}
246                 self._accountUpdateTime = datetime.datetime(1971, 1, 1)
247                 self._messages = []
248                 self._cleanMessages = []
249                 self._messageUpdateTime = datetime.datetime(1971, 1, 1)
250                 self._history = []
251                 self._historyUpdateTime = datetime.datetime(1971, 1, 1)
252                 self._dnd = False
253                 self._callback = ""
254
255         @property
256         def state(self):
257                 return {
258                         self._LOGGEDOUT_TIME: self.LOGGEDOUT_STATE,
259                         self._LOGGINGIN_TIME: self.LOGGINGIN_STATE,
260                 }.get(self._loggedInTime, self.LOGGEDIN_STATE)
261
262         @property
263         def draft(self):
264                 return self._draft
265
266         def login(self, username, password):
267                 assert self.state == self.LOGGEDOUT_STATE, "Can only log-in when logged out (currently %s" % self.state
268                 assert username != "", "No username specified"
269                 if self._cachePath is not None:
270                         cookiePath = os.path.join(self._cachePath, "%s.cookies" % username)
271                 else:
272                         cookiePath = None
273
274                 if self._username != username or not self._backend:
275                         from backends import gv_backend
276                         del self._backend[:]
277                         self._backend[0:0] = [gv_backend.GVDialer(cookiePath)]
278
279                 self._pool.start()
280                 le = concurrent.AsyncLinearExecution(self._pool, self._login)
281                 le.start(username, password)
282
283         def logout(self):
284                 assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state
285                 _moduleLogger.info("Logging out")
286                 self._pool.stop()
287                 self._loggedInTime = self._LOGGEDOUT_TIME
288                 self._backend[0].persist()
289                 self._save_to_cache()
290                 self._clear_voicemail_cache()
291                 self.stateChange.emit(self.LOGGEDOUT_STATE)
292                 self.loggedOut.emit()
293
294         def clear(self):
295                 assert self.state == self.LOGGEDOUT_STATE, "Can only clear when logged out (currently %s" % self.state
296                 self._backend[0].logout()
297                 del self._backend[0]
298                 self._clear_cache()
299                 self._draft.clear()
300
301         def logout_and_clear(self):
302                 assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state
303                 _moduleLogger.info("Logging out and clearing the account")
304                 self._pool.stop()
305                 self._loggedInTime = self._LOGGEDOUT_TIME
306                 self.clear()
307                 self.stateChange.emit(self.LOGGEDOUT_STATE)
308                 self.loggedOut.emit()
309
310         def update_account(self, force = True):
311                 if not force and self._contacts:
312                         return
313                 le = concurrent.AsyncLinearExecution(self._pool, self._update_account), (), {}
314                 self._perform_op_while_loggedin(le)
315
316         def refresh_connection(self):
317                 le = concurrent.AsyncLinearExecution(self._pool, self._refresh_authentication)
318                 le.start()
319
320         def get_contacts(self):
321                 return self._contacts
322
323         def get_when_contacts_updated(self):
324                 return self._accountUpdateTime
325
326         def update_messages(self, messageType, force = True):
327                 if not force and self._messages:
328                         return
329                 le = concurrent.AsyncLinearExecution(self._pool, self._update_messages), (messageType, ), {}
330                 self._perform_op_while_loggedin(le)
331
332         def get_messages(self):
333                 return self._messages
334
335         def get_when_messages_updated(self):
336                 return self._messageUpdateTime
337
338         def update_history(self, force = True):
339                 if not force and self._history:
340                         return
341                 le = concurrent.AsyncLinearExecution(self._pool, self._update_history), (), {}
342                 self._perform_op_while_loggedin(le)
343
344         def get_history(self):
345                 return self._history
346
347         def get_when_history_updated(self):
348                 return self._historyUpdateTime
349
350         def update_dnd(self):
351                 le = concurrent.AsyncLinearExecution(self._pool, self._update_dnd), (), {}
352                 self._perform_op_while_loggedin(le)
353
354         def set_dnd(self, dnd):
355                 le = concurrent.AsyncLinearExecution(self._pool, self._set_dnd)
356                 le.start(dnd)
357
358         def is_available(self, messageId):
359                 actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId)
360                 return os.path.exists(actualPath)
361
362         def voicemail_path(self, messageId):
363                 actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId)
364                 if not os.path.exists(actualPath):
365                         raise RuntimeError("Voicemail not available")
366                 return actualPath
367
368         def download_voicemail(self, messageId):
369                 le = concurrent.AsyncLinearExecution(self._pool, self._download_voicemail)
370                 le.start(messageId)
371
372         def _set_dnd(self, dnd):
373                 oldDnd = self._dnd
374                 try:
375                         assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state
376                         with qui_utils.notify_busy(self._errorLog, "Setting DND Status"):
377                                 yield (
378                                         self._backend[0].set_dnd,
379                                         (dnd, ),
380                                         {},
381                                 )
382                 except Exception, e:
383                         _moduleLogger.exception("Reporting error to user")
384                         self.error.emit(str(e))
385                         return
386                 self._dnd = dnd
387                 if oldDnd != self._dnd:
388                         self.dndStateChange.emit(self._dnd)
389
390         def get_dnd(self):
391                 return self._dnd
392
393         def get_account_number(self):
394                 if self.state != self.LOGGEDIN_STATE:
395                         return ""
396                 return self._backend[0].get_account_number()
397
398         def get_callback_numbers(self):
399                 if self.state != self.LOGGEDIN_STATE:
400                         return {}
401                 return self._backend[0].get_callback_numbers()
402
403         def get_callback_number(self):
404                 return self._callback
405
406         def set_callback_number(self, callback):
407                 le = concurrent.AsyncLinearExecution(self._pool, self._set_callback_number)
408                 le.start(callback)
409
410         def _set_callback_number(self, callback):
411                 oldCallback = self._callback
412                 try:
413                         assert self.state == self.LOGGEDIN_STATE, "Callbacks configurable only when logged in (currently %s" % self.state
414                         yield (
415                                 self._backend[0].set_callback_number,
416                                 (callback, ),
417                                 {},
418                         )
419                 except Exception, e:
420                         _moduleLogger.exception("Reporting error to user")
421                         self.error.emit(str(e))
422                         return
423                 self._callback = callback
424                 if oldCallback != self._callback:
425                         self.callbackNumberChanged.emit(self._callback)
426
427         def _login(self, username, password):
428                 with qui_utils.notify_busy(self._errorLog, "Logging In"):
429                         self._loggedInTime = self._LOGGINGIN_TIME
430                         self.stateChange.emit(self.LOGGINGIN_STATE)
431                         finalState = self.LOGGEDOUT_STATE
432                         accountData = None
433                         try:
434                                 if accountData is None and self._backend[0].is_quick_login_possible():
435                                         accountData = yield (
436                                                 self._backend[0].refresh_account_info,
437                                                 (),
438                                                 {},
439                                         )
440                                         if accountData is not None:
441                                                 _moduleLogger.info("Logged in through cookies")
442                                         else:
443                                                 # Force a clearing of the cookies
444                                                 yield (
445                                                         self._backend[0].logout,
446                                                         (),
447                                                         {},
448                                                 )
449
450                                 if accountData is None:
451                                         accountData = yield (
452                                                 self._backend[0].login,
453                                                 (username, password),
454                                                 {},
455                                         )
456                                         if accountData is not None:
457                                                 _moduleLogger.info("Logged in through credentials")
458
459                                 if accountData is not None:
460                                         self._loggedInTime = int(time.time())
461                                         oldUsername = self._username
462                                         self._username = username
463                                         self._password = password
464                                         finalState = self.LOGGEDIN_STATE
465                                         if oldUsername != self._username:
466                                                 needOps = not self._load()
467                                         else:
468                                                 needOps = True
469
470                                         self._voicemailCachePath = os.path.join(self._cachePath, "%s.voicemail.cache" % self._username)
471                                         try:
472                                                 os.makedirs(self._voicemailCachePath)
473                                         except OSError, e:
474                                                 if e.errno != 17:
475                                                         raise
476
477                                         self.loggedIn.emit()
478                                         self.stateChange.emit(finalState)
479                                         finalState = None # Mark it as already set
480                                         self._process_account_data(accountData)
481
482                                         if needOps:
483                                                 loginOps = self._loginOps[:]
484                                         else:
485                                                 loginOps = []
486                                         del self._loginOps[:]
487                                         for asyncOp, args, kwds in loginOps:
488                                                 asyncOp.start(*args, **kwds)
489                                 else:
490                                         self._loggedInTime = self._LOGGEDOUT_TIME
491                                         self.error.emit("Error logging in")
492                         except Exception, e:
493                                 _moduleLogger.exception("Booh")
494                                 self._loggedInTime = self._LOGGEDOUT_TIME
495                                 _moduleLogger.exception("Reporting error to user")
496                                 self.error.emit(str(e))
497                         finally:
498                                 if finalState is not None:
499                                         self.stateChange.emit(finalState)
500                         if accountData is not None and self._callback:
501                                 self.set_callback_number(self._callback)
502
503         def _update_account(self):
504                 try:
505                         with qui_utils.notify_busy(self._errorLog, "Updating Account"):
506                                 accountData = yield (
507                                         self._backend[0].refresh_account_info,
508                                         (),
509                                         {},
510                                 )
511                 except Exception, e:
512                         _moduleLogger.exception("Reporting error to user")
513                         self.error.emit(str(e))
514                         return
515                 self._loggedInTime = int(time.time())
516                 self._process_account_data(accountData)
517
518         def _refresh_authentication(self):
519                 try:
520                         with qui_utils.notify_busy(self._errorLog, "Updating Account"):
521                                 accountData = yield (
522                                         self._backend[0].refresh_account_info,
523                                         (),
524                                         {},
525                                 )
526                                 accountData = None
527                 except Exception, e:
528                         _moduleLogger.exception("Passing to user")
529                         self.error.emit(str(e))
530                         # refresh_account_info does not normally throw, so it is fine if we
531                         # just quit early because something seriously wrong is going on
532                         return
533
534                 if accountData is not None:
535                         self._loggedInTime = int(time.time())
536                         self._process_account_data(accountData)
537                 else:
538                         self._delayedRelogin.start()
539
540         def _load(self):
541                 updateMessages = len(self._messages) != 0
542                 updateHistory = len(self._history) != 0
543                 oldDnd = self._dnd
544                 oldCallback = self._callback
545
546                 self._messages = []
547                 self._cleanMessages = []
548                 self._history = []
549                 self._dnd = False
550                 self._callback = ""
551
552                 loadedFromCache = self._load_from_cache()
553                 if loadedFromCache:
554                         updateMessages = True
555                         updateHistory = True
556
557                 if updateMessages:
558                         self.messagesUpdated.emit()
559                 if updateHistory:
560                         self.historyUpdated.emit()
561                 if oldDnd != self._dnd:
562                         self.dndStateChange.emit(self._dnd)
563                 if oldCallback != self._callback:
564                         self.callbackNumberChanged.emit(self._callback)
565
566                 return loadedFromCache
567
568         def _load_from_cache(self):
569                 if self._cachePath is None:
570                         return False
571                 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
572
573                 try:
574                         with open(cachePath, "rb") as f:
575                                 dumpedData = pickle.load(f)
576                 except (pickle.PickleError, IOError, EOFError, ValueError, ImportError):
577                         _moduleLogger.exception("Pickle fun loading")
578                         return False
579                 except:
580                         _moduleLogger.exception("Weirdness loading")
581                         return False
582
583                 try:
584                         version, build = dumpedData[0:2]
585                 except ValueError:
586                         _moduleLogger.exception("Upgrade/downgrade fun")
587                         return False
588                 except:
589                         _moduleLogger.exception("Weirdlings")
590                         return False
591
592                 if misc_utils.compare_versions(
593                         self._OLDEST_COMPATIBLE_FORMAT_VERSION,
594                         misc_utils.parse_version(version),
595                 ) <= 0:
596                         try:
597                                 (
598                                         version, build,
599                                         messages, messageUpdateTime,
600                                         history, historyUpdateTime,
601                                         dnd, callback
602                                 ) = dumpedData
603                         except ValueError:
604                                 _moduleLogger.exception("Upgrade/downgrade fun")
605                                 return False
606                         except:
607                                 _moduleLogger.exception("Weirdlings")
608                                 return False
609
610                         _moduleLogger.info("Loaded cache")
611                         self._messages = messages
612                         self._alert_on_messages(self._messages)
613                         self._messageUpdateTime = messageUpdateTime
614                         self._history = history
615                         self._historyUpdateTime = historyUpdateTime
616                         self._dnd = dnd
617                         self._callback = callback
618                         return True
619                 else:
620                         _moduleLogger.debug(
621                                 "Skipping cache due to version mismatch (%s-%s)" % (
622                                         version, build
623                                 )
624                         )
625                         return False
626
627         def _save_to_cache(self):
628                 _moduleLogger.info("Saving cache")
629                 if self._cachePath is None:
630                         return
631                 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
632
633                 try:
634                         dataToDump = (
635                                 constants.__version__, constants.__build__,
636                                 self._messages, self._messageUpdateTime,
637                                 self._history, self._historyUpdateTime,
638                                 self._dnd, self._callback
639                         )
640                         with open(cachePath, "wb") as f:
641                                 pickle.dump(dataToDump, f, pickle.HIGHEST_PROTOCOL)
642                         _moduleLogger.info("Cache saved")
643                 except (pickle.PickleError, IOError):
644                         _moduleLogger.exception("While saving")
645
646         def _clear_cache(self):
647                 updateMessages = len(self._messages) != 0
648                 updateHistory = len(self._history) != 0
649                 oldDnd = self._dnd
650                 oldCallback = self._callback
651
652                 self._messages = []
653                 self._messageUpdateTime = datetime.datetime(1971, 1, 1)
654                 self._history = []
655                 self._historyUpdateTime = datetime.datetime(1971, 1, 1)
656                 self._dnd = False
657                 self._callback = ""
658
659                 if updateMessages:
660                         self.messagesUpdated.emit()
661                 if updateHistory:
662                         self.historyUpdated.emit()
663                 if oldDnd != self._dnd:
664                         self.dndStateChange.emit(self._dnd)
665                 if oldCallback != self._callback:
666                         self.callbackNumberChanged.emit(self._callback)
667
668                 self._save_to_cache()
669                 self._clear_voicemail_cache()
670
671         def _clear_voicemail_cache(self):
672                 import shutil
673                 shutil.rmtree(self._voicemailCachePath, True)
674
675         def _update_messages(self, messageType):
676                 try:
677                         assert self.state == self.LOGGEDIN_STATE, "Messages requires being logged in (currently %s" % self.state
678                         with qui_utils.notify_busy(self._errorLog, "Updating %s Messages" % messageType):
679                                 self._messages = yield (
680                                         self._backend[0].get_messages,
681                                         (messageType, ),
682                                         {},
683                                 )
684                 except Exception, e:
685                         _moduleLogger.exception("Reporting error to user")
686                         self.error.emit(str(e))
687                         return
688                 self._messageUpdateTime = datetime.datetime.now()
689                 self.messagesUpdated.emit()
690                 self._alert_on_messages(self._messages)
691
692         def _update_history(self):
693                 try:
694                         assert self.state == self.LOGGEDIN_STATE, "History requires being logged in (currently %s" % self.state
695                         with qui_utils.notify_busy(self._errorLog, "Updating History"):
696                                 self._history = yield (
697                                         self._backend[0].get_recent,
698                                         (),
699                                         {},
700                                 )
701                 except Exception, e:
702                         _moduleLogger.exception("Reporting error to user")
703                         self.error.emit(str(e))
704                         return
705                 self._historyUpdateTime = datetime.datetime.now()
706                 self.historyUpdated.emit()
707
708         def _update_dnd(self):
709                 with qui_utils.notify_busy(self._errorLog, "Updating Do-Not-Disturb Status"):
710                         oldDnd = self._dnd
711                         try:
712                                 assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state
713                                 self._dnd = yield (
714                                         self._backend[0].is_dnd,
715                                         (),
716                                         {},
717                                 )
718                         except Exception, e:
719                                 _moduleLogger.exception("Reporting error to user")
720                                 self.error.emit(str(e))
721                                 return
722                         if oldDnd != self._dnd:
723                                 self.dndStateChange(self._dnd)
724
725         def _download_voicemail(self, messageId):
726                 actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId)
727                 targetPath = "%s.%s.part" % (actualPath, time.time())
728                 if os.path.exists(actualPath):
729                         self.voicemailAvailable.emit(messageId, actualPath)
730                         return
731                 with qui_utils.notify_busy(self._errorLog, "Downloading Voicemail"):
732                         try:
733                                 yield (
734                                         self._backend[0].download,
735                                         (messageId, targetPath),
736                                         {},
737                                 )
738                         except Exception, e:
739                                 _moduleLogger.exception("Passing to user")
740                                 self.error.emit(str(e))
741                                 return
742
743                 if os.path.exists(actualPath):
744                         try:
745                                 os.remove(targetPath)
746                         except:
747                                 _moduleLogger.exception("Ignoring file problems with cache")
748                         self.voicemailAvailable.emit(messageId, actualPath)
749                         return
750                 else:
751                         os.rename(targetPath, actualPath)
752                         self.voicemailAvailable.emit(messageId, actualPath)
753
754         def _perform_op_while_loggedin(self, op):
755                 if self.state == self.LOGGEDIN_STATE:
756                         op, args, kwds = op
757                         op.start(*args, **kwds)
758                 else:
759                         self._push_login_op(op)
760
761         def _push_login_op(self, asyncOp):
762                 assert self.state != self.LOGGEDIN_STATE, "Can only queue work when logged out"
763                 if asyncOp in self._loginOps:
764                         _moduleLogger.info("Skipping queueing duplicate op: %r" % asyncOp)
765                         return
766                 self._loginOps.append(asyncOp)
767
768         def _process_account_data(self, accountData):
769                 self._contacts = dict(
770                         (contactId, contactDetails)
771                         for contactId, contactDetails in accountData["contacts"].iteritems()
772                         # A zero contact id is the catch all for unknown contacts
773                         if contactId != "0"
774                 )
775
776                 self._accountUpdateTime = datetime.datetime.now()
777                 self.accountUpdated.emit()
778
779         def _alert_on_messages(self, messages):
780                 cleanNewMessages = list(self._clean_messages(messages))
781                 cleanNewMessages.sort(key=lambda m: m["contactId"])
782                 if self._cleanMessages:
783                         if self._cleanMessages != cleanNewMessages:
784                                 self.newMessages.emit()
785                 self._cleanMessages = cleanNewMessages
786
787         def _clean_messages(self, messages):
788                 for message in messages:
789                         cleaned = dict(
790                                 kv
791                                 for kv in message.iteritems()
792                                 if kv[0] not in
793                                 [
794                                         "relTime",
795                                         "time",
796                                         "isArchived",
797                                         "isRead",
798                                         "isSpam",
799                                         "isTrash",
800                                 ]
801                         )
802
803                         # Don't let outbound messages cause alerts, especially if the package has only outbound
804                         cleaned["messageParts"] = [
805                                 tuple(part[0:-1]) for part in cleaned["messageParts"] if part[0] != "Me:"
806                         ]
807                         if not cleaned["messageParts"]:
808                                 continue
809
810                         yield cleaned
811
812         @misc_utils.log_exception(_moduleLogger)
813         def _on_delayed_relogin(self):
814                 try:
815                         username = self._username
816                         password = self._password
817                         self.logout()
818                         self.login(username, password)
819                 except Exception, e:
820                         _moduleLogger.exception("Passing to user")
821                         self.error.emit(str(e))
822                         return