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), "No contacts selected"
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, "Must select 1 and only 1 contact"
72 (contact, ) = self._contacts.itervalues()
73 number = misc_utils.make_ugly(contact.selectedNumber)
74 le = concurrent.AsyncLinearExecution(self._pool, self._call)
78 le = concurrent.AsyncLinearExecution(self._pool, self._cancel)
81 def add_contact(self, contactId, title, description, numbersWithDescriptions):
82 if contactId in self._contacts:
83 _moduleLogger.info("Adding duplicate contact %r" % contactId)
84 # @todo Remove this evil hack to re-popup the dialog
85 self.recipientsChanged.emit()
87 contactDetails = _DraftContact(title, description, numbersWithDescriptions)
88 self._contacts[contactId] = contactDetails
89 self.recipientsChanged.emit()
91 def remove_contact(self, contactId):
92 assert contactId in self._contacts, "Contact missing"
93 del self._contacts[contactId]
94 self.recipientsChanged.emit()
96 def get_contacts(self):
97 return self._contacts.iterkeys()
99 def get_num_contacts(self):
100 return len(self._contacts)
102 def get_title(self, cid):
103 return self._contacts[cid].title
105 def get_description(self, cid):
106 return self._contacts[cid].description
108 def get_numbers(self, cid):
109 return self._contacts[cid].numbers
111 def get_selected_number(self, cid):
112 return self._contacts[cid].selectedNumber
114 def set_selected_number(self, cid, number):
115 # @note I'm lazy, this isn't firing any kind of signal since only one
116 # controller right now and that is the viewer
117 assert number in (nWD[0] for nWD in self._contacts[cid].numbers), "Number not selectable"
118 self._contacts[cid].selectedNumber = number
121 oldContacts = self._contacts
124 self.recipientsChanged.emit()
126 def _send(self, numbers, text):
127 self.sendingMessage.emit()
129 with notify_busy(self._errorLog, "Sending Text"):
131 self._backend[0].send_sms,
135 self.sentMessage.emit()
138 self.error.emit(str(e))
140 def _call(self, number):
143 with notify_busy(self._errorLog, "Calling"):
145 self._backend[0].call,
152 self.error.emit(str(e))
155 self.cancelling.emit()
157 with notify_busy(self._errorLog, "Cancelling"):
159 self._backend[0].cancel,
163 self.cancelled.emit()
165 self.error.emit(str(e))
168 class Session(QtCore.QObject):
170 # @todo Somehow add support for csv contacts
172 stateChange = QtCore.pyqtSignal(str)
173 loggedOut = QtCore.pyqtSignal()
174 loggedIn = QtCore.pyqtSignal()
175 callbackNumberChanged = QtCore.pyqtSignal(str)
177 contactsUpdated = QtCore.pyqtSignal()
178 messagesUpdated = QtCore.pyqtSignal()
179 historyUpdated = QtCore.pyqtSignal()
180 dndStateChange = QtCore.pyqtSignal(bool)
182 error = QtCore.pyqtSignal(str)
184 LOGGEDOUT_STATE = "logged out"
185 LOGGINGIN_STATE = "logging in"
186 LOGGEDIN_STATE = "logged in"
188 _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.1.90")
193 def __init__(self, errorLog, cachePath = None):
194 QtCore.QObject.__init__(self)
195 self._errorLog = errorLog
196 self._pool = qore_utils.AsyncPool()
198 self._loggedInTime = self._LOGGEDOUT_TIME
200 self._cachePath = cachePath
201 self._username = None
202 self._draft = Draft(self._pool, self._backend, self._errorLog)
205 self._contactUpdateTime = datetime.datetime(1971, 1, 1)
207 self._messageUpdateTime = datetime.datetime(1971, 1, 1)
209 self._historyUpdateTime = datetime.datetime(1971, 1, 1)
216 self._LOGGEDOUT_TIME: self.LOGGEDOUT_STATE,
217 self._LOGGINGIN_TIME: self.LOGGINGIN_STATE,
218 }.get(self._loggedInTime, self.LOGGEDIN_STATE)
224 def login(self, username, password):
225 assert self.state == self.LOGGEDOUT_STATE, "Can only log-in when logged out (currently %s" % self.state
226 assert username != "", "No username specified"
227 if self._cachePath is not None:
228 cookiePath = os.path.join(self._cachePath, "%s.cookies" % username)
232 if self._username != username or not self._backend:
233 from backends import gv_backend
235 self._backend[0:0] = [gv_backend.GVDialer(cookiePath)]
238 le = concurrent.AsyncLinearExecution(self._pool, self._login)
239 le.start(username, password)
242 assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state
244 self._loggedInTime = self._LOGGEDOUT_TIME
245 self._backend[0].persist()
246 self._save_to_cache()
249 assert self.state == self.LOGGEDOUT_STATE, "Can only clear when logged out (currently %s" % self.state
250 self._backend[0].logout()
255 def logout_and_clear(self):
256 assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state
258 self._loggedInTime = self._LOGGEDOUT_TIME
261 def update_contacts(self, force = True):
262 if not force and self._contacts:
264 le = concurrent.AsyncLinearExecution(self._pool, self._update_contacts)
265 self._perform_op_while_loggedin(le)
267 def get_contacts(self):
268 return self._contacts
270 def get_when_contacts_updated(self):
271 return self._contactUpdateTime
273 def update_messages(self, force = True):
274 if not force and self._messages:
276 le = concurrent.AsyncLinearExecution(self._pool, self._update_messages)
277 self._perform_op_while_loggedin(le)
279 def get_messages(self):
280 return self._messages
282 def get_when_messages_updated(self):
283 return self._messageUpdateTime
285 def update_history(self, force = True):
286 if not force and self._history:
288 le = concurrent.AsyncLinearExecution(self._pool, self._update_history)
289 self._perform_op_while_loggedin(le)
291 def get_history(self):
294 def get_when_history_updated(self):
295 return self._historyUpdateTime
297 def update_dnd(self):
298 le = concurrent.AsyncLinearExecution(self._pool, self._update_dnd)
299 self._perform_op_while_loggedin(le)
301 def set_dnd(self, dnd):
302 le = concurrent.AsyncLinearExecution(self._pool, self._set_dnd)
305 def _set_dnd(self, dnd):
306 # I'm paranoid about our state geting out of sync so we set no matter
307 # what but act as if we have the cannonical state
308 assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state
311 with notify_busy(self._errorLog, "Setting DND Status"):
313 self._backend[0].set_dnd,
318 self.error.emit(str(e))
321 if oldDnd != self._dnd:
322 self.dndStateChange.emit(self._dnd)
327 def get_account_number(self):
328 return self._backend[0].get_account_number()
330 def get_callback_numbers(self):
331 # @todo Remove evilness (might call is_authed which can block)
332 return self._backend[0].get_callback_numbers()
334 def get_callback_number(self):
335 return self._callback
337 def set_callback_number(self, callback):
338 le = concurrent.AsyncLinearExecution(self._pool, self._set_callback_number)
341 def _set_callback_number(self, callback):
342 # I'm paranoid about our state geting out of sync so we set no matter
343 # what but act as if we have the cannonical state
344 assert self.state == self.LOGGEDIN_STATE, "Callbacks configurable only when logged in (currently %s" % self.state
345 oldCallback = self._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
366 if not isLoggedIn and self._backend[0].is_quick_login_possible():
368 self._backend[0].is_authed,
373 _moduleLogger.info("Logged in through cookies")
375 # Force a clearing of the cookies
377 self._backend[0].logout,
384 self._backend[0].login,
385 (username, password),
389 _moduleLogger.info("Logged in through credentials")
392 self._loggedInTime = int(time.time())
393 oldUsername = self._username
394 self._username = username
395 finalState = self.LOGGEDIN_STATE
396 if oldUsername != self._username:
397 needOps = not self._load()
404 loginOps = self._loginOps[:]
407 del self._loginOps[:]
408 for asyncOp in loginOps:
411 self._loggedInTime = self._LOGGEDOUT_TIME
412 self.error.emit("Error logging in")
414 self._loggedInTime = self._LOGGEDOUT_TIME
415 self.error.emit(str(e))
417 self.stateChange.emit(finalState)
418 if isLoggedIn and self._callback:
419 self.set_callback_number(self._callback)
422 updateContacts = len(self._contacts) != 0
423 updateMessages = len(self._messages) != 0
424 updateHistory = len(self._history) != 0
426 oldCallback = self._callback
434 loadedFromCache = self._load_from_cache()
436 updateContacts = True
437 updateMessages = True
441 self.contactsUpdated.emit()
443 self.messagesUpdated.emit()
445 self.historyUpdated.emit()
446 if oldDnd != self._dnd:
447 self.dndStateChange.emit(self._dnd)
448 if oldCallback != self._callback:
449 self.callbackNumberChanged.emit(self._callback)
451 return loadedFromCache
453 def _load_from_cache(self):
454 if self._cachePath is None:
456 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
459 with open(cachePath, "rb") as f:
460 dumpedData = pickle.load(f)
461 except (pickle.PickleError, IOError, EOFError, ValueError):
462 _moduleLogger.exception("Pickle fun loading")
465 _moduleLogger.exception("Weirdness loading")
470 contacts, contactUpdateTime,
471 messages, messageUpdateTime,
472 history, historyUpdateTime,
476 if misc_utils.compare_versions(
477 self._OLDEST_COMPATIBLE_FORMAT_VERSION,
478 misc_utils.parse_version(version),
480 _moduleLogger.info("Loaded cache")
481 self._contacts = contacts
482 self._contactUpdateTime = contactUpdateTime
483 self._messages = messages
484 self._messageUpdateTime = messageUpdateTime
485 self._history = history
486 self._historyUpdateTime = historyUpdateTime
488 self._callback = callback
492 "Skipping cache due to version mismatch (%s-%s)" % (
498 def _save_to_cache(self):
499 _moduleLogger.info("Saving cache")
500 if self._cachePath is None:
502 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
506 constants.__version__, constants.__build__,
507 self._contacts, self._contactUpdateTime,
508 self._messages, self._messageUpdateTime,
509 self._history, self._historyUpdateTime,
510 self._dnd, self._callback
512 with open(cachePath, "wb") as f:
513 pickle.dump(dataToDump, f, pickle.HIGHEST_PROTOCOL)
514 _moduleLogger.info("Cache saved")
515 except (pickle.PickleError, IOError):
516 _moduleLogger.exception("While saving")
518 def _clear_cache(self):
519 updateContacts = len(self._contacts) != 0
520 updateMessages = len(self._messages) != 0
521 updateHistory = len(self._history) != 0
523 oldCallback = self._callback
526 self._contactUpdateTime = datetime.datetime(1, 1, 1)
528 self._messageUpdateTime = datetime.datetime(1, 1, 1)
530 self._historyUpdateTime = datetime.datetime(1, 1, 1)
535 self.contactsUpdated.emit()
537 self.messagesUpdated.emit()
539 self.historyUpdated.emit()
540 if oldDnd != self._dnd:
541 self.dndStateChange.emit(self._dnd)
542 if oldCallback != self._callback:
543 self.callbackNumberChanged.emit(self._callback)
545 self._save_to_cache()
547 def _update_contacts(self):
549 with notify_busy(self._errorLog, "Updating Contacts"):
550 self._contacts = yield (
551 self._backend[0].get_contacts,
556 self.error.emit(str(e))
558 self._contactUpdateTime = datetime.datetime.now()
559 self.contactsUpdated.emit()
561 def _update_messages(self):
563 with notify_busy(self._errorLog, "Updating Messages"):
564 self._messages = yield (
565 self._backend[0].get_messages,
570 self.error.emit(str(e))
572 self._messageUpdateTime = datetime.datetime.now()
573 self.messagesUpdated.emit()
575 def _update_history(self):
577 with notify_busy(self._errorLog, "Updating History"):
578 self._history = yield (
579 self._backend[0].get_recent,
584 self.error.emit(str(e))
586 self._historyUpdateTime = datetime.datetime.now()
587 self.historyUpdated.emit()
589 def _update_dnd(self):
593 self._backend[0].is_dnd,
598 self.error.emit(str(e))
600 if oldDnd != self._dnd:
601 self.dndStateChange(self._dnd)
603 def _perform_op_while_loggedin(self, op):
604 if self.state == self.LOGGEDIN_STATE:
607 self._push_login_op(op)
609 def _push_login_op(self, asyncOp):
610 assert self.state != self.LOGGEDIN_STATE, "Can only queue work when logged out"
611 if asyncOp in self._loginOps:
612 _moduleLogger.info("Skipping queueing duplicate op: %r" % asyncOp)
614 self._loginOps.append(asyncOp)