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