1 from __future__ import with_statement
15 from PyQt4 import QtCore
17 from util import qore_utils
18 from util import concurrent
19 from util import misc as misc_utils
24 _moduleLogger = logging.getLogger(__name__)
27 @contextlib.contextmanager
28 def notify_busy(log, message):
29 log.push_busy(message)
36 class _DraftContact(object):
38 def __init__(self, title, description, numbersWithDescriptions):
40 self.description = description
41 self.numbers = numbersWithDescriptions
42 self.selectedNumber = numbersWithDescriptions[0][0]
45 class Draft(QtCore.QObject):
47 sendingMessage = QtCore.pyqtSignal()
48 sentMessage = QtCore.pyqtSignal()
49 calling = QtCore.pyqtSignal()
50 called = QtCore.pyqtSignal()
51 cancelling = QtCore.pyqtSignal()
52 cancelled = QtCore.pyqtSignal()
53 error = QtCore.pyqtSignal(str)
55 recipientsChanged = QtCore.pyqtSignal()
57 def __init__(self, pool, backend, errorLog):
58 QtCore.QObject.__init__(self)
59 self._errorLog = errorLog
62 self._backend = backend
65 assert 0 < len(self._contacts)
66 numbers = [misc_utils.make_ugly(contact.selectedNumber) for contact in self._contacts.itervalues()]
67 le = concurrent.AsyncLinearExecution(self._pool, self._send)
68 le.start(numbers, text)
71 assert len(self._contacts) == 1
72 (contact, ) = self._contacts.itervalues()
73 le = concurrent.AsyncLinearExecution(self._pool, self._call)
74 le.start(contact.selectedNumber)
77 le = concurrent.AsyncLinearExecution(self._pool, self._cancel)
80 def add_contact(self, contactId, title, description, numbersWithDescriptions):
81 if contactId in self._contacts:
82 _moduleLogger.info("Adding duplicate contact %r" % contactId)
83 # @todo Remove this evil hack to re-popup the dialog
84 self.recipientsChanged.emit()
86 contactDetails = _DraftContact(title, description, numbersWithDescriptions)
87 self._contacts[contactId] = contactDetails
88 self.recipientsChanged.emit()
90 def remove_contact(self, contactId):
91 assert contactId in self._contacts
92 del self._contacts[contactId]
93 self.recipientsChanged.emit()
95 def get_contacts(self):
96 return self._contacts.iterkeys()
98 def get_num_contacts(self):
99 return len(self._contacts)
101 def get_title(self, cid):
102 return self._contacts[cid].title
104 def get_description(self, cid):
105 return self._contacts[cid].description
107 def get_numbers(self, cid):
108 return self._contacts[cid].numbers
110 def get_selected_number(self, cid):
111 return self._contacts[cid].selectedNumber
113 def set_selected_number(self, cid, number):
114 # @note I'm lazy, this isn't firing any kind of signal since only one
115 # controller right now and that is the viewer
116 assert number in (nWD[0] for nWD in self._contacts[cid].numbers)
117 self._contacts[cid].selectedNumber = number
120 oldContacts = self._contacts
123 self.recipientsChanged.emit()
125 def _send(self, numbers, text):
126 self.sendingMessage.emit()
128 with notify_busy(self._errorLog, "Sending Text"):
130 self._backend[0].send_sms,
134 self.sentMessage.emit()
137 self.error.emit(str(e))
139 def _call(self, number):
142 with notify_busy(self._errorLog, "Calling"):
144 self._backend[0].call,
151 self.error.emit(str(e))
154 self.cancelling.emit()
156 with notify_busy(self._errorLog, "Cancelling"):
158 self._backend[0].cancel,
162 self.cancelled.emit()
164 self.error.emit(str(e))
167 class Session(QtCore.QObject):
169 # @todo Somehow add support for csv contacts
171 stateChange = QtCore.pyqtSignal(str)
172 loggedOut = QtCore.pyqtSignal()
173 loggedIn = QtCore.pyqtSignal()
174 callbackNumberChanged = QtCore.pyqtSignal(str)
176 contactsUpdated = QtCore.pyqtSignal()
177 messagesUpdated = QtCore.pyqtSignal()
178 historyUpdated = QtCore.pyqtSignal()
179 dndStateChange = QtCore.pyqtSignal(bool)
181 error = QtCore.pyqtSignal(str)
183 LOGGEDOUT_STATE = "logged out"
184 LOGGINGIN_STATE = "logging in"
185 LOGGEDIN_STATE = "logged in"
187 _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.1.90")
192 def __init__(self, errorLog, cachePath = None):
193 QtCore.QObject.__init__(self)
194 self._errorLog = errorLog
195 self._pool = qore_utils.AsyncPool()
197 self._loggedInTime = self._LOGGEDOUT_TIME
199 self._cachePath = cachePath
200 self._username = None
201 self._draft = Draft(self._pool, self._backend, self._errorLog)
204 self._contactUpdateTime = datetime.datetime(1971, 1, 1)
206 self._messageUpdateTime = datetime.datetime(1971, 1, 1)
208 self._historyUpdateTime = datetime.datetime(1971, 1, 1)
215 self._LOGGEDOUT_TIME: self.LOGGEDOUT_STATE,
216 self._LOGGINGIN_TIME: self.LOGGINGIN_STATE,
217 }.get(self._loggedInTime, self.LOGGEDIN_STATE)
223 def login(self, username, password):
224 assert self.state == self.LOGGEDOUT_STATE
225 assert username != ""
226 if self._cachePath is not None:
227 cookiePath = os.path.join(self._cachePath, "%s.cookies" % username)
231 if self._username != username or not self._backend:
232 from backends import gv_backend
234 self._backend[0:0] = [gv_backend.GVDialer(cookiePath)]
237 le = concurrent.AsyncLinearExecution(self._pool, self._login)
238 le.start(username, password)
241 assert self.state != self.LOGGEDOUT_STATE
243 self._loggedInTime = self._LOGGEDOUT_TIME
244 self._backend[0].persist()
245 self._save_to_cache()
248 assert self.state == self.LOGGEDOUT_STATE
249 self._backend[0].logout()
254 def logout_and_clear(self):
255 assert self.state != self.LOGGEDOUT_STATE
257 self._loggedInTime = self._LOGGEDOUT_TIME
260 def update_contacts(self, force = True):
261 if not force and self._contacts:
263 le = concurrent.AsyncLinearExecution(self._pool, self._update_contacts)
264 self._perform_op_while_loggedin(le)
266 def get_contacts(self):
267 return self._contacts
269 def get_when_contacts_updated(self):
270 return self._contactUpdateTime
272 def update_messages(self, force = True):
273 if not force and self._messages:
275 le = concurrent.AsyncLinearExecution(self._pool, self._update_messages)
276 self._perform_op_while_loggedin(le)
278 def get_messages(self):
279 return self._messages
281 def get_when_messages_updated(self):
282 return self._messageUpdateTime
284 def update_history(self, force = True):
285 if not force and self._history:
287 le = concurrent.AsyncLinearExecution(self._pool, self._update_history)
288 self._perform_op_while_loggedin(le)
290 def get_history(self):
293 def get_when_history_updated(self):
294 return self._historyUpdateTime
296 def update_dnd(self):
297 le = concurrent.AsyncLinearExecution(self._pool, self._update_dnd)
298 self._perform_op_while_loggedin(le)
300 def set_dnd(self, dnd):
301 le = concurrent.AsyncLinearExecution(self._pool, self._set_dnd)
304 def _set_dnd(self, dnd):
305 # I'm paranoid about our state geting out of sync so we set no matter
306 # what but act as if we have the cannonical state
307 assert self.state == self.LOGGEDIN_STATE
310 with notify_busy(self._errorLog, "Setting DND Status"):
312 self._backend[0].set_dnd,
317 self.error.emit(str(e))
320 if oldDnd != self._dnd:
321 self.dndStateChange.emit(self._dnd)
326 def get_account_number(self):
327 return self._backend[0].get_account_number()
329 def get_callback_numbers(self):
330 # @todo Remove evilness (might call is_authed which can block)
331 return self._backend[0].get_callback_numbers()
333 def get_callback_number(self):
334 return self._callback
336 def set_callback_number(self, callback):
337 le = concurrent.AsyncLinearExecution(self._pool, self._set_callback_number)
340 def _set_callback_number(self, callback):
341 # I'm paranoid about our state geting out of sync so we set no matter
342 # what but act as if we have the cannonical state
343 assert self.state == self.LOGGEDIN_STATE
344 oldCallback = self._callback
346 with notify_busy(self._errorLog, "Setting Callback"):
348 self._backend[0].set_callback_number,
353 self.error.emit(str(e))
355 self._callback = callback
356 if oldCallback != self._callback:
357 self.callbackNumberChanged.emit(self._callback)
359 def _login(self, username, password):
360 with notify_busy(self._errorLog, "Logging In"):
361 self._loggedInTime = self._LOGGINGIN_TIME
362 self.stateChange.emit(self.LOGGINGIN_STATE)
363 finalState = self.LOGGEDOUT_STATE
367 if not isLoggedIn and self._backend[0].is_quick_login_possible():
369 self._backend[0].is_authed,
374 _moduleLogger.info("Logged in through cookies")
376 # Force a clearing of the cookies
378 self._backend[0].logout,
385 self._backend[0].login,
386 (username, password),
390 _moduleLogger.info("Logged in through credentials")
393 self._loggedInTime = int(time.time())
394 oldUsername = self._username
395 self._username = username
396 finalState = self.LOGGEDIN_STATE
398 if oldUsername != self._username:
399 needOps = not self._load()
403 loginOps = self._loginOps[:]
406 del self._loginOps[:]
407 for asyncOp in loginOps:
410 self.error.emit(str(e))
412 self.stateChange.emit(finalState)
415 updateContacts = len(self._contacts) != 0
416 updateMessages = len(self._messages) != 0
417 updateHistory = len(self._history) != 0
419 oldCallback = self._callback
427 loadedFromCache = self._load_from_cache()
429 updateContacts = True
430 updateMessages = True
434 self.contactsUpdated.emit()
436 self.messagesUpdated.emit()
438 self.historyUpdated.emit()
439 if oldDnd != self._dnd:
440 self.dndStateChange.emit(self._dnd)
441 if oldCallback != self._callback:
442 self.callbackNumberChanged.emit(self._callback)
444 return loadedFromCache
446 def _load_from_cache(self):
447 if self._cachePath is None:
449 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
452 with open(cachePath, "rb") as f:
453 dumpedData = pickle.load(f)
454 except (pickle.PickleError, IOError, EOFError, ValueError):
455 _moduleLogger.exception("Pickle fun loading")
458 _moduleLogger.exception("Weirdness loading")
463 contacts, contactUpdateTime,
464 messages, messageUpdateTime,
465 history, historyUpdateTime,
469 if misc_utils.compare_versions(
470 self._OLDEST_COMPATIBLE_FORMAT_VERSION,
471 misc_utils.parse_version(version),
473 _moduleLogger.info("Loaded cache")
474 self._contacts = contacts
475 self._contactUpdateTime = contactUpdateTime
476 self._messages = messages
477 self._messageUpdateTime = messageUpdateTime
478 self._history = history
479 self._historyUpdateTime = historyUpdateTime
481 self._callback = callback
485 "Skipping cache due to version mismatch (%s-%s)" % (
491 def _save_to_cache(self):
492 _moduleLogger.info("Saving cache")
493 if self._cachePath is None:
495 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
499 constants.__version__, constants.__build__,
500 self._contacts, self._contactUpdateTime,
501 self._messages, self._messageUpdateTime,
502 self._history, self._historyUpdateTime,
503 self._dnd, self._callback
505 with open(cachePath, "wb") as f:
506 pickle.dump(dataToDump, f, pickle.HIGHEST_PROTOCOL)
507 _moduleLogger.info("Cache saved")
508 except (pickle.PickleError, IOError):
509 _moduleLogger.exception("While saving")
511 def _clear_cache(self):
512 updateContacts = len(self._contacts) != 0
513 updateMessages = len(self._messages) != 0
514 updateHistory = len(self._history) != 0
516 oldCallback = self._callback
519 self._contactUpdateTime = datetime.datetime(1, 1, 1)
521 self._messageUpdateTime = datetime.datetime(1, 1, 1)
523 self._historyUpdateTime = datetime.datetime(1, 1, 1)
528 self.contactsUpdated.emit()
530 self.messagesUpdated.emit()
532 self.historyUpdated.emit()
533 if oldDnd != self._dnd:
534 self.dndStateChange.emit(self._dnd)
535 if oldCallback != self._callback:
536 self.callbackNumberChanged.emit(self._callback)
538 self._save_to_cache()
540 def _update_contacts(self):
542 with notify_busy(self._errorLog, "Updating Contacts"):
543 self._contacts = yield (
544 self._backend[0].get_contacts,
549 self.error.emit(str(e))
551 self._contactUpdateTime = datetime.datetime.now()
552 self.contactsUpdated.emit()
554 def _update_messages(self):
556 with notify_busy(self._errorLog, "Updating Messages"):
557 self._messages = yield (
558 self._backend[0].get_messages,
563 self.error.emit(str(e))
565 self._messageUpdateTime = datetime.datetime.now()
566 self.messagesUpdated.emit()
568 def _update_history(self):
570 with notify_busy(self._errorLog, "Updating History"):
571 self._history = yield (
572 self._backend[0].get_recent,
577 self.error.emit(str(e))
579 self._historyUpdateTime = datetime.datetime.now()
580 self.historyUpdated.emit()
582 def _update_dnd(self):
586 self._backend[0].is_dnd,
591 self.error.emit(str(e))
593 if oldDnd != self._dnd:
594 self.dndStateChange(self._dnd)
596 def _perform_op_while_loggedin(self, op):
597 if self.state == self.LOGGEDIN_STATE:
600 self._push_login_op(op)
602 def _push_login_op(self, asyncOp):
603 assert self.state != self.LOGGEDIN_STATE
604 if asyncOp in self._loginOps:
605 _moduleLogger.info("Skipping queueing duplicate op: %r" % asyncOp)
607 self._loginOps.append(asyncOp)