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