X-Git-Url: http://git.maemo.org/git/?p=gc-dialer;a=blobdiff_plain;f=src%2Fsession.py;h=7061f09b383c145b7736e922711547c57f7044a9;hp=7697bf122b572cdfb4666cca7b3df97ae5a7f26b;hb=3cb332b757e1001a8d03920ce44f8885f562a963;hpb=9babd7f38132f837d9b08432a5ab91802e7802d8 diff --git a/src/session.py b/src/session.py index 7697bf1..7061f09 100644 --- a/src/session.py +++ b/src/session.py @@ -27,7 +27,8 @@ _moduleLogger = logging.getLogger(__name__) class _DraftContact(object): - def __init__(self, title, description, numbersWithDescriptions): + def __init__(self, messageId, title, description, numbersWithDescriptions): + self.messageId = messageId self.title = title self.description = description self.numbers = numbersWithDescriptions @@ -82,15 +83,11 @@ class Draft(QtCore.QObject): message = property(_get_message, _set_message) - def add_contact(self, contactId, title, description, numbersWithDescriptions): + def add_contact(self, contactId, messageId, title, description, numbersWithDescriptions): if self._busyReason is not None: raise RuntimeError("Please wait for %r" % self._busyReason) - if contactId in self._contacts: - _moduleLogger.info("Adding duplicate contact %r" % contactId) - # @todo Remove this evil hack to re-popup the dialog - self.recipientsChanged.emit() - return - contactDetails = _DraftContact(title, description, numbersWithDescriptions) + # Allow overwriting of contacts so that the message can be updated and the SMS dialog popped back up + contactDetails = _DraftContact(messageId, title, description, numbersWithDescriptions) self._contacts[contactId] = contactDetails self.recipientsChanged.emit() @@ -107,6 +104,9 @@ class Draft(QtCore.QObject): def get_num_contacts(self): return len(self._contacts) + def get_message_id(self, cid): + return self._contacts[cid].messageId + def get_title(self, cid): return self._contacts[cid].title @@ -160,6 +160,7 @@ class Draft(QtCore.QObject): self.sentMessage.emit() self._clear() except Exception, e: + _moduleLogger.exception("Reporting error to user") self.error.emit(str(e)) def _call(self, number): @@ -175,6 +176,7 @@ class Draft(QtCore.QObject): self.called.emit() self._clear() except Exception, e: + _moduleLogger.exception("Reporting error to user") self.error.emit(str(e)) def _cancel(self): @@ -188,6 +190,7 @@ class Draft(QtCore.QObject): ) self.cancelled.emit() except Exception, e: + _moduleLogger.exception("Reporting error to user") self.error.emit(str(e)) @@ -200,10 +203,12 @@ class Session(QtCore.QObject): loggedIn = QtCore.pyqtSignal() callbackNumberChanged = QtCore.pyqtSignal(str) - contactsUpdated = QtCore.pyqtSignal() + accountUpdated = QtCore.pyqtSignal() messagesUpdated = QtCore.pyqtSignal() + newMessages = QtCore.pyqtSignal() historyUpdated = QtCore.pyqtSignal() dndStateChange = QtCore.pyqtSignal(bool) + voicemailAvailable = QtCore.pyqtSignal(str, str) error = QtCore.pyqtSignal(str) @@ -211,7 +216,11 @@ class Session(QtCore.QObject): LOGGINGIN_STATE = "logging in" LOGGEDIN_STATE = "logged in" - _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.1.90") + MESSAGE_TEXTS = "Text" + MESSAGE_VOICEMAILS = "Voicemail" + MESSAGE_ALL = "All" + + _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.3.0") _LOGGEDOUT_TIME = -1 _LOGGINGIN_TIME = 0 @@ -224,12 +233,19 @@ class Session(QtCore.QObject): self._loggedInTime = self._LOGGEDOUT_TIME self._loginOps = [] self._cachePath = cachePath + self._voicemailCachePath = None self._username = None + self._password = None self._draft = Draft(self._pool, self._backend, self._errorLog) + self._delayedRelogin = QtCore.QTimer() + self._delayedRelogin.setInterval(0) + self._delayedRelogin.setSingleShot(True) + self._delayedRelogin.timeout.connect(self._on_delayed_relogin) self._contacts = {} - self._contactUpdateTime = datetime.datetime(1971, 1, 1) + self._accountUpdateTime = datetime.datetime(1971, 1, 1) self._messages = [] + self._cleanMessages = [] self._messageUpdateTime = datetime.datetime(1971, 1, 1) self._history = [] self._historyUpdateTime = datetime.datetime(1971, 1, 1) @@ -266,10 +282,14 @@ class Session(QtCore.QObject): def logout(self): assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state + _moduleLogger.info("Logging out") self._pool.stop() self._loggedInTime = self._LOGGEDOUT_TIME self._backend[0].persist() self._save_to_cache() + self._clear_voicemail_cache() + self.stateChange.emit(self.LOGGEDOUT_STATE) + self.loggedOut.emit() def clear(self): assert self.state == self.LOGGEDOUT_STATE, "Can only clear when logged out (currently %s" % self.state @@ -280,26 +300,33 @@ class Session(QtCore.QObject): def logout_and_clear(self): assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state + _moduleLogger.info("Logging out and clearing the account") self._pool.stop() self._loggedInTime = self._LOGGEDOUT_TIME self.clear() + self.stateChange.emit(self.LOGGEDOUT_STATE) + self.loggedOut.emit() - def update_contacts(self, force = True): + def update_account(self, force = True): if not force and self._contacts: return - le = concurrent.AsyncLinearExecution(self._pool, self._update_contacts) + le = concurrent.AsyncLinearExecution(self._pool, self._update_account), (), {} self._perform_op_while_loggedin(le) + def refresh_connection(self): + le = concurrent.AsyncLinearExecution(self._pool, self._refresh_authentication) + le.start() + def get_contacts(self): return self._contacts def get_when_contacts_updated(self): - return self._contactUpdateTime + return self._accountUpdateTime - def update_messages(self, force = True): + def update_messages(self, messageType, force = True): if not force and self._messages: return - le = concurrent.AsyncLinearExecution(self._pool, self._update_messages) + le = concurrent.AsyncLinearExecution(self._pool, self._update_messages), (messageType, ), {} self._perform_op_while_loggedin(le) def get_messages(self): @@ -311,7 +338,7 @@ class Session(QtCore.QObject): def update_history(self, force = True): if not force and self._history: return - le = concurrent.AsyncLinearExecution(self._pool, self._update_history) + le = concurrent.AsyncLinearExecution(self._pool, self._update_history), (), {} self._perform_op_while_loggedin(le) def get_history(self): @@ -321,19 +348,31 @@ class Session(QtCore.QObject): return self._historyUpdateTime def update_dnd(self): - le = concurrent.AsyncLinearExecution(self._pool, self._update_dnd) + le = concurrent.AsyncLinearExecution(self._pool, self._update_dnd), (), {} self._perform_op_while_loggedin(le) def set_dnd(self, dnd): le = concurrent.AsyncLinearExecution(self._pool, self._set_dnd) le.start(dnd) + def is_available(self, messageId): + actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId) + return os.path.exists(actualPath) + + def voicemail_path(self, messageId): + actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId) + if not os.path.exists(actualPath): + raise RuntimeError("Voicemail not available") + return actualPath + + def download_voicemail(self, messageId): + le = concurrent.AsyncLinearExecution(self._pool, self._download_voicemail) + le.start(messageId) + def _set_dnd(self, dnd): - # I'm paranoid about our state geting out of sync so we set no matter - # what but act as if we have the cannonical state - assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state oldDnd = self._dnd try: + assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state with qui_utils.notify_busy(self._errorLog, "Setting DND Status"): yield ( self._backend[0].set_dnd, @@ -341,6 +380,7 @@ class Session(QtCore.QObject): {}, ) except Exception, e: + _moduleLogger.exception("Reporting error to user") self.error.emit(str(e)) return self._dnd = dnd @@ -351,10 +391,13 @@ class Session(QtCore.QObject): return self._dnd def get_account_number(self): + if self.state != self.LOGGEDIN_STATE: + return "" return self._backend[0].get_account_number() def get_callback_numbers(self): - # @todo Remove evilness (might call is_authed which can block) + if self.state != self.LOGGEDIN_STATE: + return {} return self._backend[0].get_callback_numbers() def get_callback_number(self): @@ -365,17 +408,16 @@ class Session(QtCore.QObject): le.start(callback) def _set_callback_number(self, callback): - # I'm paranoid about our state geting out of sync so we set no matter - # what but act as if we have the cannonical state - assert self.state == self.LOGGEDIN_STATE, "Callbacks configurable only when logged in (currently %s" % self.state oldCallback = self._callback try: + assert self.state == self.LOGGEDIN_STATE, "Callbacks configurable only when logged in (currently %s" % self.state yield ( self._backend[0].set_callback_number, (callback, ), {}, ) except Exception, e: + _moduleLogger.exception("Reporting error to user") self.error.emit(str(e)) return self._callback = callback @@ -387,15 +429,15 @@ class Session(QtCore.QObject): self._loggedInTime = self._LOGGINGIN_TIME self.stateChange.emit(self.LOGGINGIN_STATE) finalState = self.LOGGEDOUT_STATE - isLoggedIn = False + accountData = None try: - if not isLoggedIn and self._backend[0].is_quick_login_possible(): - isLoggedIn = yield ( - self._backend[0].is_authed, + if accountData is None and self._backend[0].is_quick_login_possible(): + accountData = yield ( + self._backend[0].refresh_account_info, (), {}, ) - if isLoggedIn: + if accountData is not None: _moduleLogger.info("Logged in through cookies") else: # Force a clearing of the cookies @@ -405,66 +447,113 @@ class Session(QtCore.QObject): {}, ) - if not isLoggedIn: - isLoggedIn = yield ( + if accountData is None: + accountData = yield ( self._backend[0].login, (username, password), {}, ) - if isLoggedIn: + if accountData is not None: _moduleLogger.info("Logged in through credentials") - if isLoggedIn: + if accountData is not None: self._loggedInTime = int(time.time()) oldUsername = self._username self._username = username + self._password = password finalState = self.LOGGEDIN_STATE if oldUsername != self._username: needOps = not self._load() else: needOps = True + self._voicemailCachePath = os.path.join(self._cachePath, "%s.voicemail.cache" % self._username) + try: + os.makedirs(self._voicemailCachePath) + except OSError, e: + if e.errno != 17: + raise + self.loggedIn.emit() + self.stateChange.emit(finalState) + finalState = None # Mark it as already set + self._process_account_data(accountData) if needOps: loginOps = self._loginOps[:] else: loginOps = [] del self._loginOps[:] - for asyncOp in loginOps: - asyncOp.start() + for asyncOp, args, kwds in loginOps: + asyncOp.start(*args, **kwds) else: self._loggedInTime = self._LOGGEDOUT_TIME self.error.emit("Error logging in") except Exception, e: + _moduleLogger.exception("Booh") self._loggedInTime = self._LOGGEDOUT_TIME + _moduleLogger.exception("Reporting error to user") self.error.emit(str(e)) finally: - self.stateChange.emit(finalState) - if isLoggedIn and self._callback: + if finalState is not None: + self.stateChange.emit(finalState) + if accountData is not None and self._callback: self.set_callback_number(self._callback) + def _update_account(self): + try: + with qui_utils.notify_busy(self._errorLog, "Updating Account"): + accountData = yield ( + self._backend[0].refresh_account_info, + (), + {}, + ) + except Exception, e: + _moduleLogger.exception("Reporting error to user") + self.error.emit(str(e)) + return + self._loggedInTime = int(time.time()) + self._process_account_data(accountData) + + def _refresh_authentication(self): + try: + with qui_utils.notify_busy(self._errorLog, "Updating Account"): + accountData = yield ( + self._backend[0].refresh_account_info, + (), + {}, + ) + accountData = None + except Exception, e: + _moduleLogger.exception("Passing to user") + self.error.emit(str(e)) + # refresh_account_info does not normally throw, so it is fine if we + # just quit early because something seriously wrong is going on + return + + if accountData is not None: + self._loggedInTime = int(time.time()) + self._process_account_data(accountData) + else: + self._delayedRelogin.start() + def _load(self): - updateContacts = len(self._contacts) != 0 updateMessages = len(self._messages) != 0 updateHistory = len(self._history) != 0 oldDnd = self._dnd oldCallback = self._callback - self._contacts = {} self._messages = [] + self._cleanMessages = [] self._history = [] self._dnd = False self._callback = "" loadedFromCache = self._load_from_cache() if loadedFromCache: - updateContacts = True updateMessages = True updateHistory = True - if updateContacts: - self.contactsUpdated.emit() if updateMessages: self.messagesUpdated.emit() if updateHistory: @@ -484,29 +573,43 @@ class Session(QtCore.QObject): try: with open(cachePath, "rb") as f: dumpedData = pickle.load(f) - except (pickle.PickleError, IOError, EOFError, ValueError): + except (pickle.PickleError, IOError, EOFError, ValueError, ImportError): _moduleLogger.exception("Pickle fun loading") return False except: _moduleLogger.exception("Weirdness loading") return False - ( - version, build, - contacts, contactUpdateTime, - messages, messageUpdateTime, - history, historyUpdateTime, - dnd, callback - ) = dumpedData + try: + version, build = dumpedData[0:2] + except ValueError: + _moduleLogger.exception("Upgrade/downgrade fun") + return False + except: + _moduleLogger.exception("Weirdlings") + return False if misc_utils.compare_versions( self._OLDEST_COMPATIBLE_FORMAT_VERSION, misc_utils.parse_version(version), ) <= 0: + try: + ( + version, build, + messages, messageUpdateTime, + history, historyUpdateTime, + dnd, callback + ) = dumpedData + except ValueError: + _moduleLogger.exception("Upgrade/downgrade fun") + return False + except: + _moduleLogger.exception("Weirdlings") + return False + _moduleLogger.info("Loaded cache") - self._contacts = contacts - self._contactUpdateTime = contactUpdateTime self._messages = messages + self._alert_on_messages(self._messages) self._messageUpdateTime = messageUpdateTime self._history = history self._historyUpdateTime = historyUpdateTime @@ -530,7 +633,6 @@ class Session(QtCore.QObject): try: dataToDump = ( constants.__version__, constants.__build__, - self._contacts, self._contactUpdateTime, self._messages, self._messageUpdateTime, self._history, self._historyUpdateTime, self._dnd, self._callback @@ -542,14 +644,11 @@ class Session(QtCore.QObject): _moduleLogger.exception("While saving") def _clear_cache(self): - updateContacts = len(self._contacts) != 0 updateMessages = len(self._messages) != 0 updateHistory = len(self._history) != 0 oldDnd = self._dnd oldCallback = self._callback - self._contacts = {} - self._contactUpdateTime = datetime.datetime(1971, 1, 1) self._messages = [] self._messageUpdateTime = datetime.datetime(1971, 1, 1) self._history = [] @@ -557,8 +656,6 @@ class Session(QtCore.QObject): self._dnd = False self._callback = "" - if updateContacts: - self.contactsUpdated.emit() if updateMessages: self.messagesUpdated.emit() if updateHistory: @@ -569,66 +666,95 @@ class Session(QtCore.QObject): self.callbackNumberChanged.emit(self._callback) self._save_to_cache() + self._clear_voicemail_cache() - def _update_contacts(self): - try: - with qui_utils.notify_busy(self._errorLog, "Updating Contacts"): - self._contacts = yield ( - self._backend[0].get_contacts, - (), - {}, - ) - except Exception, e: - self.error.emit(str(e)) - return - self._contactUpdateTime = datetime.datetime.now() - self.contactsUpdated.emit() + def _clear_voicemail_cache(self): + import shutil + shutil.rmtree(self._voicemailCachePath, True) - def _update_messages(self): + def _update_messages(self, messageType): try: - with qui_utils.notify_busy(self._errorLog, "Updating Messages"): + assert self.state == self.LOGGEDIN_STATE, "Messages requires being logged in (currently %s" % self.state + with qui_utils.notify_busy(self._errorLog, "Updating %s Messages" % messageType): self._messages = yield ( self._backend[0].get_messages, - (), + (messageType, ), {}, ) except Exception, e: + _moduleLogger.exception("Reporting error to user") self.error.emit(str(e)) return self._messageUpdateTime = datetime.datetime.now() self.messagesUpdated.emit() + self._alert_on_messages(self._messages) def _update_history(self): try: + assert self.state == self.LOGGEDIN_STATE, "History requires being logged in (currently %s" % self.state with qui_utils.notify_busy(self._errorLog, "Updating History"): self._history = yield ( - self._backend[0].get_recent, - (), + self._backend[0].get_call_history, + (self._backend[0].HISTORY_ALL, ), {}, ) except Exception, e: + _moduleLogger.exception("Reporting error to user") self.error.emit(str(e)) return self._historyUpdateTime = datetime.datetime.now() self.historyUpdated.emit() def _update_dnd(self): - oldDnd = self._dnd - try: - self._dnd = yield ( - self._backend[0].is_dnd, - (), - {}, - ) - except Exception, e: - self.error.emit(str(e)) + with qui_utils.notify_busy(self._errorLog, "Updating Do-Not-Disturb Status"): + oldDnd = self._dnd + try: + assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state + self._dnd = yield ( + self._backend[0].is_dnd, + (), + {}, + ) + except Exception, e: + _moduleLogger.exception("Reporting error to user") + self.error.emit(str(e)) + return + if oldDnd != self._dnd: + self.dndStateChange(self._dnd) + + def _download_voicemail(self, messageId): + actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId) + targetPath = "%s.%s.part" % (actualPath, time.time()) + if os.path.exists(actualPath): + self.voicemailAvailable.emit(messageId, actualPath) return - if oldDnd != self._dnd: - self.dndStateChange(self._dnd) + with qui_utils.notify_busy(self._errorLog, "Downloading Voicemail"): + try: + yield ( + self._backend[0].download, + (messageId, targetPath), + {}, + ) + except Exception, e: + _moduleLogger.exception("Passing to user") + self.error.emit(str(e)) + return + + if os.path.exists(actualPath): + try: + os.remove(targetPath) + except: + _moduleLogger.exception("Ignoring file problems with cache") + self.voicemailAvailable.emit(messageId, actualPath) + return + else: + os.rename(targetPath, actualPath) + self.voicemailAvailable.emit(messageId, actualPath) def _perform_op_while_loggedin(self, op): if self.state == self.LOGGEDIN_STATE: - op.start() + op, args, kwds = op + op.start(*args, **kwds) else: self._push_login_op(op) @@ -638,3 +764,59 @@ class Session(QtCore.QObject): _moduleLogger.info("Skipping queueing duplicate op: %r" % asyncOp) return self._loginOps.append(asyncOp) + + def _process_account_data(self, accountData): + self._contacts = dict( + (contactId, contactDetails) + for contactId, contactDetails in accountData["contacts"].iteritems() + # A zero contact id is the catch all for unknown contacts + if contactId != "0" + ) + + self._accountUpdateTime = datetime.datetime.now() + self.accountUpdated.emit() + + def _alert_on_messages(self, messages): + cleanNewMessages = list(self._clean_messages(messages)) + cleanNewMessages.sort(key=lambda m: m["contactId"]) + if self._cleanMessages: + if self._cleanMessages != cleanNewMessages: + self.newMessages.emit() + self._cleanMessages = cleanNewMessages + + def _clean_messages(self, messages): + for message in messages: + cleaned = dict( + kv + for kv in message.iteritems() + if kv[0] not in + [ + "relTime", + "time", + "isArchived", + "isRead", + "isSpam", + "isTrash", + ] + ) + + # Don't let outbound messages cause alerts, especially if the package has only outbound + cleaned["messageParts"] = [ + tuple(part[0:-1]) for part in cleaned["messageParts"] if part[0] != "Me:" + ] + if not cleaned["messageParts"]: + continue + + yield cleaned + + @misc_utils.log_exception(_moduleLogger) + def _on_delayed_relogin(self): + try: + username = self._username + password = self._password + self.logout() + self.login(username, password) + except Exception, e: + _moduleLogger.exception("Passing to user") + self.error.emit(str(e)) + return