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