Front-end support for partial history updates
[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         HISTORY_RECEIVED = "Received"
224         HISTORY_MISSED = "Missed"
225         HISTORY_PLACED = "Placed"
226         HISTORY_ALL = "All"
227
228         _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.3.0")
229
230         _LOGGEDOUT_TIME = -1
231         _LOGGINGIN_TIME = 0
232
233         def __init__(self, errorLog, cachePath = None):
234                 QtCore.QObject.__init__(self)
235                 self._errorLog = errorLog
236                 self._pool = qore_utils.AsyncPool()
237                 self._backend = []
238                 self._loggedInTime = self._LOGGEDOUT_TIME
239                 self._loginOps = []
240                 self._cachePath = cachePath
241                 self._voicemailCachePath = None
242                 self._username = None
243                 self._password = None
244                 self._draft = Draft(self._pool, self._backend, self._errorLog)
245                 self._delayedRelogin = QtCore.QTimer()
246                 self._delayedRelogin.setInterval(0)
247                 self._delayedRelogin.setSingleShot(True)
248                 self._delayedRelogin.timeout.connect(self._on_delayed_relogin)
249
250                 self._contacts = {}
251                 self._accountUpdateTime = datetime.datetime(1971, 1, 1)
252                 self._messages = []
253                 self._cleanMessages = []
254                 self._messageUpdateTime = datetime.datetime(1971, 1, 1)
255                 self._history = []
256                 self._historyUpdateTime = datetime.datetime(1971, 1, 1)
257                 self._dnd = False
258                 self._callback = ""
259
260         @property
261         def state(self):
262                 return {
263                         self._LOGGEDOUT_TIME: self.LOGGEDOUT_STATE,
264                         self._LOGGINGIN_TIME: self.LOGGINGIN_STATE,
265                 }.get(self._loggedInTime, self.LOGGEDIN_STATE)
266
267         @property
268         def draft(self):
269                 return self._draft
270
271         def login(self, username, password):
272                 assert self.state == self.LOGGEDOUT_STATE, "Can only log-in when logged out (currently %s" % self.state
273                 assert username != "", "No username specified"
274                 if self._cachePath is not None:
275                         cookiePath = os.path.join(self._cachePath, "%s.cookies" % username)
276                 else:
277                         cookiePath = None
278
279                 if self._username != username or not self._backend:
280                         from backends import gv_backend
281                         del self._backend[:]
282                         self._backend[0:0] = [gv_backend.GVDialer(cookiePath)]
283
284                 self._pool.start()
285                 le = concurrent.AsyncLinearExecution(self._pool, self._login)
286                 le.start(username, password)
287
288         def logout(self):
289                 assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state
290                 _moduleLogger.info("Logging out")
291                 self._pool.stop()
292                 self._loggedInTime = self._LOGGEDOUT_TIME
293                 self._backend[0].persist()
294                 self._save_to_cache()
295                 self._clear_voicemail_cache()
296                 self.stateChange.emit(self.LOGGEDOUT_STATE)
297                 self.loggedOut.emit()
298
299         def clear(self):
300                 assert self.state == self.LOGGEDOUT_STATE, "Can only clear when logged out (currently %s" % self.state
301                 self._backend[0].logout()
302                 del self._backend[0]
303                 self._clear_cache()
304                 self._draft.clear()
305
306         def logout_and_clear(self):
307                 assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state
308                 _moduleLogger.info("Logging out and clearing the account")
309                 self._pool.stop()
310                 self._loggedInTime = self._LOGGEDOUT_TIME
311                 self.clear()
312                 self.stateChange.emit(self.LOGGEDOUT_STATE)
313                 self.loggedOut.emit()
314
315         def update_account(self, force = True):
316                 if not force and self._contacts:
317                         return
318                 le = concurrent.AsyncLinearExecution(self._pool, self._update_account), (), {}
319                 self._perform_op_while_loggedin(le)
320
321         def refresh_connection(self):
322                 le = concurrent.AsyncLinearExecution(self._pool, self._refresh_authentication)
323                 le.start()
324
325         def get_contacts(self):
326                 return self._contacts
327
328         def get_when_contacts_updated(self):
329                 return self._accountUpdateTime
330
331         def update_messages(self, messageType, force = True):
332                 if not force and self._messages:
333                         return
334                 le = concurrent.AsyncLinearExecution(self._pool, self._update_messages), (messageType, ), {}
335                 self._perform_op_while_loggedin(le)
336
337         def get_messages(self):
338                 return self._messages
339
340         def get_when_messages_updated(self):
341                 return self._messageUpdateTime
342
343         def update_history(self, historyType, force = True):
344                 if not force and self._history:
345                         return
346                 le = concurrent.AsyncLinearExecution(self._pool, self._update_history), (historyType, ), {}
347                 self._perform_op_while_loggedin(le)
348
349         def get_history(self):
350                 return self._history
351
352         def get_when_history_updated(self):
353                 return self._historyUpdateTime
354
355         def update_dnd(self):
356                 le = concurrent.AsyncLinearExecution(self._pool, self._update_dnd), (), {}
357                 self._perform_op_while_loggedin(le)
358
359         def set_dnd(self, dnd):
360                 le = concurrent.AsyncLinearExecution(self._pool, self._set_dnd)
361                 le.start(dnd)
362
363         def is_available(self, messageId):
364                 actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId)
365                 return os.path.exists(actualPath)
366
367         def voicemail_path(self, messageId):
368                 actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId)
369                 if not os.path.exists(actualPath):
370                         raise RuntimeError("Voicemail not available")
371                 return actualPath
372
373         def download_voicemail(self, messageId):
374                 le = concurrent.AsyncLinearExecution(self._pool, self._download_voicemail)
375                 le.start(messageId)
376
377         def _set_dnd(self, dnd):
378                 oldDnd = self._dnd
379                 try:
380                         assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state
381                         with qui_utils.notify_busy(self._errorLog, "Setting DND Status"):
382                                 yield (
383                                         self._backend[0].set_dnd,
384                                         (dnd, ),
385                                         {},
386                                 )
387                 except Exception, e:
388                         _moduleLogger.exception("Reporting error to user")
389                         self.error.emit(str(e))
390                         return
391                 self._dnd = dnd
392                 if oldDnd != self._dnd:
393                         self.dndStateChange.emit(self._dnd)
394
395         def get_dnd(self):
396                 return self._dnd
397
398         def get_account_number(self):
399                 if self.state != self.LOGGEDIN_STATE:
400                         return ""
401                 return self._backend[0].get_account_number()
402
403         def get_callback_numbers(self):
404                 if self.state != self.LOGGEDIN_STATE:
405                         return {}
406                 return self._backend[0].get_callback_numbers()
407
408         def get_callback_number(self):
409                 return self._callback
410
411         def set_callback_number(self, callback):
412                 le = concurrent.AsyncLinearExecution(self._pool, self._set_callback_number)
413                 le.start(callback)
414
415         def _set_callback_number(self, callback):
416                 oldCallback = self._callback
417                 try:
418                         assert self.state == self.LOGGEDIN_STATE, "Callbacks configurable only when logged in (currently %s" % self.state
419                         yield (
420                                 self._backend[0].set_callback_number,
421                                 (callback, ),
422                                 {},
423                         )
424                 except Exception, e:
425                         _moduleLogger.exception("Reporting error to user")
426                         self.error.emit(str(e))
427                         return
428                 self._callback = callback
429                 if oldCallback != self._callback:
430                         self.callbackNumberChanged.emit(self._callback)
431
432         def _login(self, username, password):
433                 with qui_utils.notify_busy(self._errorLog, "Logging In"):
434                         self._loggedInTime = self._LOGGINGIN_TIME
435                         self.stateChange.emit(self.LOGGINGIN_STATE)
436                         finalState = self.LOGGEDOUT_STATE
437                         accountData = None
438                         try:
439                                 if accountData is None and self._backend[0].is_quick_login_possible():
440                                         accountData = yield (
441                                                 self._backend[0].refresh_account_info,
442                                                 (),
443                                                 {},
444                                         )
445                                         if accountData is not None:
446                                                 _moduleLogger.info("Logged in through cookies")
447                                         else:
448                                                 # Force a clearing of the cookies
449                                                 yield (
450                                                         self._backend[0].logout,
451                                                         (),
452                                                         {},
453                                                 )
454
455                                 if accountData is None:
456                                         accountData = yield (
457                                                 self._backend[0].login,
458                                                 (username, password),
459                                                 {},
460                                         )
461                                         if accountData is not None:
462                                                 _moduleLogger.info("Logged in through credentials")
463
464                                 if accountData is not None:
465                                         self._loggedInTime = int(time.time())
466                                         oldUsername = self._username
467                                         self._username = username
468                                         self._password = password
469                                         finalState = self.LOGGEDIN_STATE
470                                         if oldUsername != self._username:
471                                                 needOps = not self._load()
472                                         else:
473                                                 needOps = True
474
475                                         self._voicemailCachePath = os.path.join(self._cachePath, "%s.voicemail.cache" % self._username)
476                                         try:
477                                                 os.makedirs(self._voicemailCachePath)
478                                         except OSError, e:
479                                                 if e.errno != 17:
480                                                         raise
481
482                                         self.loggedIn.emit()
483                                         self.stateChange.emit(finalState)
484                                         finalState = None # Mark it as already set
485                                         self._process_account_data(accountData)
486
487                                         if needOps:
488                                                 loginOps = self._loginOps[:]
489                                         else:
490                                                 loginOps = []
491                                         del self._loginOps[:]
492                                         for asyncOp, args, kwds in loginOps:
493                                                 asyncOp.start(*args, **kwds)
494                                 else:
495                                         self._loggedInTime = self._LOGGEDOUT_TIME
496                                         self.error.emit("Error logging in")
497                         except Exception, e:
498                                 _moduleLogger.exception("Booh")
499                                 self._loggedInTime = self._LOGGEDOUT_TIME
500                                 _moduleLogger.exception("Reporting error to user")
501                                 self.error.emit(str(e))
502                         finally:
503                                 if finalState is not None:
504                                         self.stateChange.emit(finalState)
505                         if accountData is not None and self._callback:
506                                 self.set_callback_number(self._callback)
507
508         def _update_account(self):
509                 try:
510                         with qui_utils.notify_busy(self._errorLog, "Updating Account"):
511                                 accountData = yield (
512                                         self._backend[0].refresh_account_info,
513                                         (),
514                                         {},
515                                 )
516                 except Exception, e:
517                         _moduleLogger.exception("Reporting error to user")
518                         self.error.emit(str(e))
519                         return
520                 self._loggedInTime = int(time.time())
521                 self._process_account_data(accountData)
522
523         def _refresh_authentication(self):
524                 try:
525                         with qui_utils.notify_busy(self._errorLog, "Updating Account"):
526                                 accountData = yield (
527                                         self._backend[0].refresh_account_info,
528                                         (),
529                                         {},
530                                 )
531                                 accountData = None
532                 except Exception, e:
533                         _moduleLogger.exception("Passing to user")
534                         self.error.emit(str(e))
535                         # refresh_account_info does not normally throw, so it is fine if we
536                         # just quit early because something seriously wrong is going on
537                         return
538
539                 if accountData is not None:
540                         self._loggedInTime = int(time.time())
541                         self._process_account_data(accountData)
542                 else:
543                         self._delayedRelogin.start()
544
545         def _load(self):
546                 updateMessages = len(self._messages) != 0
547                 updateHistory = len(self._history) != 0
548                 oldDnd = self._dnd
549                 oldCallback = self._callback
550
551                 self._messages = []
552                 self._cleanMessages = []
553                 self._history = []
554                 self._dnd = False
555                 self._callback = ""
556
557                 loadedFromCache = self._load_from_cache()
558                 if loadedFromCache:
559                         updateMessages = True
560                         updateHistory = True
561
562                 if updateMessages:
563                         self.messagesUpdated.emit()
564                 if updateHistory:
565                         self.historyUpdated.emit()
566                 if oldDnd != self._dnd:
567                         self.dndStateChange.emit(self._dnd)
568                 if oldCallback != self._callback:
569                         self.callbackNumberChanged.emit(self._callback)
570
571                 return loadedFromCache
572
573         def _load_from_cache(self):
574                 if self._cachePath is None:
575                         return False
576                 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
577
578                 try:
579                         with open(cachePath, "rb") as f:
580                                 dumpedData = pickle.load(f)
581                 except (pickle.PickleError, IOError, EOFError, ValueError, ImportError):
582                         _moduleLogger.exception("Pickle fun loading")
583                         return False
584                 except:
585                         _moduleLogger.exception("Weirdness loading")
586                         return False
587
588                 try:
589                         version, build = dumpedData[0:2]
590                 except ValueError:
591                         _moduleLogger.exception("Upgrade/downgrade fun")
592                         return False
593                 except:
594                         _moduleLogger.exception("Weirdlings")
595                         return False
596
597                 if misc_utils.compare_versions(
598                         self._OLDEST_COMPATIBLE_FORMAT_VERSION,
599                         misc_utils.parse_version(version),
600                 ) <= 0:
601                         try:
602                                 (
603                                         version, build,
604                                         messages, messageUpdateTime,
605                                         history, historyUpdateTime,
606                                         dnd, callback
607                                 ) = dumpedData
608                         except ValueError:
609                                 _moduleLogger.exception("Upgrade/downgrade fun")
610                                 return False
611                         except:
612                                 _moduleLogger.exception("Weirdlings")
613                                 return False
614
615                         _moduleLogger.info("Loaded cache")
616                         self._messages = messages
617                         self._alert_on_messages(self._messages)
618                         self._messageUpdateTime = messageUpdateTime
619                         self._history = history
620                         self._historyUpdateTime = historyUpdateTime
621                         self._dnd = dnd
622                         self._callback = callback
623                         return True
624                 else:
625                         _moduleLogger.debug(
626                                 "Skipping cache due to version mismatch (%s-%s)" % (
627                                         version, build
628                                 )
629                         )
630                         return False
631
632         def _save_to_cache(self):
633                 _moduleLogger.info("Saving cache")
634                 if self._cachePath is None:
635                         return
636                 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
637
638                 try:
639                         dataToDump = (
640                                 constants.__version__, constants.__build__,
641                                 self._messages, self._messageUpdateTime,
642                                 self._history, self._historyUpdateTime,
643                                 self._dnd, self._callback
644                         )
645                         with open(cachePath, "wb") as f:
646                                 pickle.dump(dataToDump, f, pickle.HIGHEST_PROTOCOL)
647                         _moduleLogger.info("Cache saved")
648                 except (pickle.PickleError, IOError):
649                         _moduleLogger.exception("While saving")
650
651         def _clear_cache(self):
652                 updateMessages = len(self._messages) != 0
653                 updateHistory = len(self._history) != 0
654                 oldDnd = self._dnd
655                 oldCallback = self._callback
656
657                 self._messages = []
658                 self._messageUpdateTime = datetime.datetime(1971, 1, 1)
659                 self._history = []
660                 self._historyUpdateTime = datetime.datetime(1971, 1, 1)
661                 self._dnd = False
662                 self._callback = ""
663
664                 if updateMessages:
665                         self.messagesUpdated.emit()
666                 if updateHistory:
667                         self.historyUpdated.emit()
668                 if oldDnd != self._dnd:
669                         self.dndStateChange.emit(self._dnd)
670                 if oldCallback != self._callback:
671                         self.callbackNumberChanged.emit(self._callback)
672
673                 self._save_to_cache()
674                 self._clear_voicemail_cache()
675
676         def _clear_voicemail_cache(self):
677                 import shutil
678                 shutil.rmtree(self._voicemailCachePath, True)
679
680         def _update_messages(self, messageType):
681                 try:
682                         assert self.state == self.LOGGEDIN_STATE, "Messages requires being logged in (currently %s" % self.state
683                         with qui_utils.notify_busy(self._errorLog, "Updating %s Messages" % messageType):
684                                 self._messages = yield (
685                                         self._backend[0].get_messages,
686                                         (messageType, ),
687                                         {},
688                                 )
689                 except Exception, e:
690                         _moduleLogger.exception("Reporting error to user")
691                         self.error.emit(str(e))
692                         return
693                 self._messageUpdateTime = datetime.datetime.now()
694                 self.messagesUpdated.emit()
695                 self._alert_on_messages(self._messages)
696
697         def _update_history(self, historyType):
698                 try:
699                         assert self.state == self.LOGGEDIN_STATE, "History requires being logged in (currently %s" % self.state
700                         with qui_utils.notify_busy(self._errorLog, "Updating History"):
701                                 self._history = yield (
702                                         self._backend[0].get_call_history,
703                                         (historyType, ),
704                                         {},
705                                 )
706                 except Exception, e:
707                         _moduleLogger.exception("Reporting error to user")
708                         self.error.emit(str(e))
709                         return
710                 self._historyUpdateTime = datetime.datetime.now()
711                 self.historyUpdated.emit()
712
713         def _update_dnd(self):
714                 with qui_utils.notify_busy(self._errorLog, "Updating Do-Not-Disturb Status"):
715                         oldDnd = self._dnd
716                         try:
717                                 assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state
718                                 self._dnd = yield (
719                                         self._backend[0].is_dnd,
720                                         (),
721                                         {},
722                                 )
723                         except Exception, e:
724                                 _moduleLogger.exception("Reporting error to user")
725                                 self.error.emit(str(e))
726                                 return
727                         if oldDnd != self._dnd:
728                                 self.dndStateChange(self._dnd)
729
730         def _download_voicemail(self, messageId):
731                 actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId)
732                 targetPath = "%s.%s.part" % (actualPath, time.time())
733                 if os.path.exists(actualPath):
734                         self.voicemailAvailable.emit(messageId, actualPath)
735                         return
736                 with qui_utils.notify_busy(self._errorLog, "Downloading Voicemail"):
737                         try:
738                                 yield (
739                                         self._backend[0].download,
740                                         (messageId, targetPath),
741                                         {},
742                                 )
743                         except Exception, e:
744                                 _moduleLogger.exception("Passing to user")
745                                 self.error.emit(str(e))
746                                 return
747
748                 if os.path.exists(actualPath):
749                         try:
750                                 os.remove(targetPath)
751                         except:
752                                 _moduleLogger.exception("Ignoring file problems with cache")
753                         self.voicemailAvailable.emit(messageId, actualPath)
754                         return
755                 else:
756                         os.rename(targetPath, actualPath)
757                         self.voicemailAvailable.emit(messageId, actualPath)
758
759         def _perform_op_while_loggedin(self, op):
760                 if self.state == self.LOGGEDIN_STATE:
761                         op, args, kwds = op
762                         op.start(*args, **kwds)
763                 else:
764                         self._push_login_op(op)
765
766         def _push_login_op(self, asyncOp):
767                 assert self.state != self.LOGGEDIN_STATE, "Can only queue work when logged out"
768                 if asyncOp in self._loginOps:
769                         _moduleLogger.info("Skipping queueing duplicate op: %r" % asyncOp)
770                         return
771                 self._loginOps.append(asyncOp)
772
773         def _process_account_data(self, accountData):
774                 self._contacts = dict(
775                         (contactId, contactDetails)
776                         for contactId, contactDetails in accountData["contacts"].iteritems()
777                         # A zero contact id is the catch all for unknown contacts
778                         if contactId != "0"
779                 )
780
781                 self._accountUpdateTime = datetime.datetime.now()
782                 self.accountUpdated.emit()
783
784         def _alert_on_messages(self, messages):
785                 cleanNewMessages = list(self._clean_messages(messages))
786                 cleanNewMessages.sort(key=lambda m: m["contactId"])
787                 if self._cleanMessages:
788                         if self._cleanMessages != cleanNewMessages:
789                                 self.newMessages.emit()
790                 self._cleanMessages = cleanNewMessages
791
792         def _clean_messages(self, messages):
793                 for message in messages:
794                         cleaned = dict(
795                                 kv
796                                 for kv in message.iteritems()
797                                 if kv[0] not in
798                                 [
799                                         "relTime",
800                                         "time",
801                                         "isArchived",
802                                         "isRead",
803                                         "isSpam",
804                                         "isTrash",
805                                 ]
806                         )
807
808                         # Don't let outbound messages cause alerts, especially if the package has only outbound
809                         cleaned["messageParts"] = [
810                                 tuple(part[0:-1]) for part in cleaned["messageParts"] if part[0] != "Me:"
811                         ]
812                         if not cleaned["messageParts"]:
813                                 continue
814
815                         yield cleaned
816
817         @misc_utils.log_exception(_moduleLogger)
818         def _on_delayed_relogin(self):
819                 try:
820                         username = self._username
821                         password = self._password
822                         self.logout()
823                         self.login(username, password)
824                 except Exception, e:
825                         _moduleLogger.exception("Passing to user")
826                         self.error.emit(str(e))
827                         return