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