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
63 self._busyReason = None
66 assert 0 < len(self._contacts), "No contacts selected"
67 numbers = [misc_utils.make_ugly(contact.selectedNumber) for contact in self._contacts.itervalues()]
68 le = concurrent.AsyncLinearExecution(self._pool, self._send)
69 le.start(numbers, text)
72 assert len(self._contacts) == 1, "Must select 1 and only 1 contact"
73 (contact, ) = self._contacts.itervalues()
74 number = misc_utils.make_ugly(contact.selectedNumber)
75 le = concurrent.AsyncLinearExecution(self._pool, self._call)
79 le = concurrent.AsyncLinearExecution(self._pool, self._cancel)
82 def add_contact(self, contactId, title, description, numbersWithDescriptions):
83 if self._busyReason is not None:
84 raise RuntimeError("Please wait for %r" % self._busyReason)
85 if contactId in self._contacts:
86 _moduleLogger.info("Adding duplicate contact %r" % contactId)
87 # @todo Remove this evil hack to re-popup the dialog
88 self.recipientsChanged.emit()
90 contactDetails = _DraftContact(title, description, numbersWithDescriptions)
91 self._contacts[contactId] = contactDetails
92 self.recipientsChanged.emit()
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()
101 def get_contacts(self):
102 return self._contacts.iterkeys()
104 def get_num_contacts(self):
105 return len(self._contacts)
107 def get_title(self, cid):
108 return self._contacts[cid].title
110 def get_description(self, cid):
111 return self._contacts[cid].description
113 def get_numbers(self, cid):
114 return self._contacts[cid].numbers
116 def get_selected_number(self, cid):
117 return self._contacts[cid].selectedNumber
119 def set_selected_number(self, cid, number):
120 # @note I'm lazy, this isn't firing any kind of signal since only one
121 # controller right now and that is the viewer
122 assert number in (nWD[0] for nWD in self._contacts[cid].numbers), "Number not selectable"
123 self._contacts[cid].selectedNumber = number
126 if self._busyReason is not None:
127 raise RuntimeError("Please wait for %r" % self._busyReason)
131 oldContacts = self._contacts
134 self.recipientsChanged.emit()
136 @contextlib.contextmanager
137 def _busy(self, message):
138 if self._busyReason is not None:
139 raise RuntimeError("Already busy doing %r" % self._busyReason)
141 self._busyReason = message
144 self._busyReason = None
146 def _send(self, numbers, text):
147 self.sendingMessage.emit()
149 with self._busy("Sending Text"):
150 with notify_busy(self._errorLog, "Sending Text"):
152 self._backend[0].send_sms,
156 self.sentMessage.emit()
159 self.error.emit(str(e))
161 def _call(self, number):
164 with self._busy("Calling"):
165 with notify_busy(self._errorLog, "Calling"):
167 self._backend[0].call,
174 self.error.emit(str(e))
177 self.cancelling.emit()
179 with notify_busy(self._errorLog, "Cancelling"):
181 self._backend[0].cancel,
185 self.cancelled.emit()
187 self.error.emit(str(e))
190 class Session(QtCore.QObject):
192 # @todo Somehow add support for csv contacts
194 stateChange = QtCore.pyqtSignal(str)
195 loggedOut = QtCore.pyqtSignal()
196 loggedIn = QtCore.pyqtSignal()
197 callbackNumberChanged = QtCore.pyqtSignal(str)
199 contactsUpdated = QtCore.pyqtSignal()
200 messagesUpdated = QtCore.pyqtSignal()
201 historyUpdated = QtCore.pyqtSignal()
202 dndStateChange = QtCore.pyqtSignal(bool)
204 error = QtCore.pyqtSignal(str)
206 LOGGEDOUT_STATE = "logged out"
207 LOGGINGIN_STATE = "logging in"
208 LOGGEDIN_STATE = "logged in"
210 _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.1.90")
215 def __init__(self, errorLog, cachePath = None):
216 QtCore.QObject.__init__(self)
217 self._errorLog = errorLog
218 self._pool = qore_utils.AsyncPool()
220 self._loggedInTime = self._LOGGEDOUT_TIME
222 self._cachePath = cachePath
223 self._username = None
224 self._draft = Draft(self._pool, self._backend, self._errorLog)
227 self._contactUpdateTime = datetime.datetime(1971, 1, 1)
229 self._messageUpdateTime = datetime.datetime(1971, 1, 1)
231 self._historyUpdateTime = datetime.datetime(1971, 1, 1)
238 self._LOGGEDOUT_TIME: self.LOGGEDOUT_STATE,
239 self._LOGGINGIN_TIME: self.LOGGINGIN_STATE,
240 }.get(self._loggedInTime, self.LOGGEDIN_STATE)
246 def login(self, username, password):
247 assert self.state == self.LOGGEDOUT_STATE, "Can only log-in when logged out (currently %s" % self.state
248 assert username != "", "No username specified"
249 if self._cachePath is not None:
250 cookiePath = os.path.join(self._cachePath, "%s.cookies" % username)
254 if self._username != username or not self._backend:
255 from backends import gv_backend
257 self._backend[0:0] = [gv_backend.GVDialer(cookiePath)]
260 le = concurrent.AsyncLinearExecution(self._pool, self._login)
261 le.start(username, password)
264 assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state
266 self._loggedInTime = self._LOGGEDOUT_TIME
267 self._backend[0].persist()
268 self._save_to_cache()
271 assert self.state == self.LOGGEDOUT_STATE, "Can only clear when logged out (currently %s" % self.state
272 self._backend[0].logout()
277 def logout_and_clear(self):
278 assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state
280 self._loggedInTime = self._LOGGEDOUT_TIME
283 def update_contacts(self, force = True):
284 if not force and self._contacts:
286 le = concurrent.AsyncLinearExecution(self._pool, self._update_contacts)
287 self._perform_op_while_loggedin(le)
289 def get_contacts(self):
290 return self._contacts
292 def get_when_contacts_updated(self):
293 return self._contactUpdateTime
295 def update_messages(self, force = True):
296 if not force and self._messages:
298 le = concurrent.AsyncLinearExecution(self._pool, self._update_messages)
299 self._perform_op_while_loggedin(le)
301 def get_messages(self):
302 return self._messages
304 def get_when_messages_updated(self):
305 return self._messageUpdateTime
307 def update_history(self, force = True):
308 if not force and self._history:
310 le = concurrent.AsyncLinearExecution(self._pool, self._update_history)
311 self._perform_op_while_loggedin(le)
313 def get_history(self):
316 def get_when_history_updated(self):
317 return self._historyUpdateTime
319 def update_dnd(self):
320 le = concurrent.AsyncLinearExecution(self._pool, self._update_dnd)
321 self._perform_op_while_loggedin(le)
323 def set_dnd(self, dnd):
324 le = concurrent.AsyncLinearExecution(self._pool, self._set_dnd)
327 def _set_dnd(self, dnd):
328 # I'm paranoid about our state geting out of sync so we set no matter
329 # what but act as if we have the cannonical state
330 assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state
333 with notify_busy(self._errorLog, "Setting DND Status"):
335 self._backend[0].set_dnd,
340 self.error.emit(str(e))
343 if oldDnd != self._dnd:
344 self.dndStateChange.emit(self._dnd)
349 def get_account_number(self):
350 return self._backend[0].get_account_number()
352 def get_callback_numbers(self):
353 # @todo Remove evilness (might call is_authed which can block)
354 return self._backend[0].get_callback_numbers()
356 def get_callback_number(self):
357 return self._callback
359 def set_callback_number(self, callback):
360 le = concurrent.AsyncLinearExecution(self._pool, self._set_callback_number)
363 def _set_callback_number(self, callback):
364 # I'm paranoid about our state geting out of sync so we set no matter
365 # what but act as if we have the cannonical state
366 assert self.state == self.LOGGEDIN_STATE, "Callbacks configurable only when logged in (currently %s" % self.state
367 oldCallback = self._callback
370 self._backend[0].set_callback_number,
375 self.error.emit(str(e))
377 self._callback = callback
378 if oldCallback != self._callback:
379 self.callbackNumberChanged.emit(self._callback)
381 def _login(self, username, password):
382 with notify_busy(self._errorLog, "Logging In"):
383 self._loggedInTime = self._LOGGINGIN_TIME
384 self.stateChange.emit(self.LOGGINGIN_STATE)
385 finalState = self.LOGGEDOUT_STATE
388 if not isLoggedIn and self._backend[0].is_quick_login_possible():
390 self._backend[0].is_authed,
395 _moduleLogger.info("Logged in through cookies")
397 # Force a clearing of the cookies
399 self._backend[0].logout,
406 self._backend[0].login,
407 (username, password),
411 _moduleLogger.info("Logged in through credentials")
414 self._loggedInTime = int(time.time())
415 oldUsername = self._username
416 self._username = username
417 finalState = self.LOGGEDIN_STATE
418 if oldUsername != self._username:
419 needOps = not self._load()
426 loginOps = self._loginOps[:]
429 del self._loginOps[:]
430 for asyncOp in loginOps:
433 self._loggedInTime = self._LOGGEDOUT_TIME
434 self.error.emit("Error logging in")
436 self._loggedInTime = self._LOGGEDOUT_TIME
437 self.error.emit(str(e))
439 self.stateChange.emit(finalState)
440 if isLoggedIn and self._callback:
441 self.set_callback_number(self._callback)
444 updateContacts = len(self._contacts) != 0
445 updateMessages = len(self._messages) != 0
446 updateHistory = len(self._history) != 0
448 oldCallback = self._callback
456 loadedFromCache = self._load_from_cache()
458 updateContacts = True
459 updateMessages = True
463 self.contactsUpdated.emit()
465 self.messagesUpdated.emit()
467 self.historyUpdated.emit()
468 if oldDnd != self._dnd:
469 self.dndStateChange.emit(self._dnd)
470 if oldCallback != self._callback:
471 self.callbackNumberChanged.emit(self._callback)
473 return loadedFromCache
475 def _load_from_cache(self):
476 if self._cachePath is None:
478 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
481 with open(cachePath, "rb") as f:
482 dumpedData = pickle.load(f)
483 except (pickle.PickleError, IOError, EOFError, ValueError):
484 _moduleLogger.exception("Pickle fun loading")
487 _moduleLogger.exception("Weirdness loading")
492 contacts, contactUpdateTime,
493 messages, messageUpdateTime,
494 history, historyUpdateTime,
498 if misc_utils.compare_versions(
499 self._OLDEST_COMPATIBLE_FORMAT_VERSION,
500 misc_utils.parse_version(version),
502 _moduleLogger.info("Loaded cache")
503 self._contacts = contacts
504 self._contactUpdateTime = contactUpdateTime
505 self._messages = messages
506 self._messageUpdateTime = messageUpdateTime
507 self._history = history
508 self._historyUpdateTime = historyUpdateTime
510 self._callback = callback
514 "Skipping cache due to version mismatch (%s-%s)" % (
520 def _save_to_cache(self):
521 _moduleLogger.info("Saving cache")
522 if self._cachePath is None:
524 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
528 constants.__version__, constants.__build__,
529 self._contacts, self._contactUpdateTime,
530 self._messages, self._messageUpdateTime,
531 self._history, self._historyUpdateTime,
532 self._dnd, self._callback
534 with open(cachePath, "wb") as f:
535 pickle.dump(dataToDump, f, pickle.HIGHEST_PROTOCOL)
536 _moduleLogger.info("Cache saved")
537 except (pickle.PickleError, IOError):
538 _moduleLogger.exception("While saving")
540 def _clear_cache(self):
541 updateContacts = len(self._contacts) != 0
542 updateMessages = len(self._messages) != 0
543 updateHistory = len(self._history) != 0
545 oldCallback = self._callback
548 self._contactUpdateTime = datetime.datetime(1, 1, 1)
550 self._messageUpdateTime = datetime.datetime(1, 1, 1)
552 self._historyUpdateTime = datetime.datetime(1, 1, 1)
557 self.contactsUpdated.emit()
559 self.messagesUpdated.emit()
561 self.historyUpdated.emit()
562 if oldDnd != self._dnd:
563 self.dndStateChange.emit(self._dnd)
564 if oldCallback != self._callback:
565 self.callbackNumberChanged.emit(self._callback)
567 self._save_to_cache()
569 def _update_contacts(self):
571 with notify_busy(self._errorLog, "Updating Contacts"):
572 self._contacts = yield (
573 self._backend[0].get_contacts,
578 self.error.emit(str(e))
580 self._contactUpdateTime = datetime.datetime.now()
581 self.contactsUpdated.emit()
583 def _update_messages(self):
585 with notify_busy(self._errorLog, "Updating Messages"):
586 self._messages = yield (
587 self._backend[0].get_messages,
592 self.error.emit(str(e))
594 self._messageUpdateTime = datetime.datetime.now()
595 self.messagesUpdated.emit()
597 def _update_history(self):
599 with notify_busy(self._errorLog, "Updating History"):
600 self._history = yield (
601 self._backend[0].get_recent,
606 self.error.emit(str(e))
608 self._historyUpdateTime = datetime.datetime.now()
609 self.historyUpdated.emit()
611 def _update_dnd(self):
615 self._backend[0].is_dnd,
620 self.error.emit(str(e))
622 if oldDnd != self._dnd:
623 self.dndStateChange(self._dnd)
625 def _perform_op_while_loggedin(self, op):
626 if self.state == self.LOGGEDIN_STATE:
629 self._push_login_op(op)
631 def _push_login_op(self, asyncOp):
632 assert self.state != self.LOGGEDIN_STATE, "Can only queue work when logged out"
633 if asyncOp in self._loginOps:
634 _moduleLogger.info("Skipping queueing duplicate op: %r" % asyncOp)
636 self._loginOps.append(asyncOp)