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