1 from __future__ import with_statement
15 from PyQt4 import QtCore
17 from util import qore_utils
18 from util import qui_utils
19 from util import concurrent
20 from util import misc as misc_utils
25 _moduleLogger = logging.getLogger(__name__)
28 class _DraftContact(object):
30 def __init__(self, title, description, numbersWithDescriptions):
32 self.description = description
33 self.numbers = numbersWithDescriptions
34 self.selectedNumber = numbersWithDescriptions[0][0]
37 class Draft(QtCore.QObject):
39 sendingMessage = QtCore.pyqtSignal()
40 sentMessage = QtCore.pyqtSignal()
41 calling = QtCore.pyqtSignal()
42 called = QtCore.pyqtSignal()
43 cancelling = QtCore.pyqtSignal()
44 cancelled = QtCore.pyqtSignal()
45 error = QtCore.pyqtSignal(str)
47 recipientsChanged = QtCore.pyqtSignal()
49 def __init__(self, pool, backend, errorLog):
50 QtCore.QObject.__init__(self)
51 self._errorLog = errorLog
54 self._backend = backend
55 self._busyReason = None
59 assert 0 < len(self._contacts), "No contacts selected"
60 assert 0 < len(self._message), "No message to send"
61 numbers = [misc_utils.make_ugly(contact.selectedNumber) for contact in self._contacts.itervalues()]
62 le = concurrent.AsyncLinearExecution(self._pool, self._send)
63 le.start(numbers, self._message)
66 assert len(self._contacts) == 1, "Must select 1 and only 1 contact"
67 assert len(self._message) == 0, "Cannot send message with call"
68 (contact, ) = self._contacts.itervalues()
69 number = misc_utils.make_ugly(contact.selectedNumber)
70 le = concurrent.AsyncLinearExecution(self._pool, self._call)
74 le = concurrent.AsyncLinearExecution(self._pool, self._cancel)
77 def _get_message(self):
80 def _set_message(self, message):
81 if self._busyReason is not None:
82 raise RuntimeError("Please wait for %r" % self._busyReason)
83 self._message = message
85 message = property(_get_message, _set_message)
87 def add_contact(self, contactId, title, description, numbersWithDescriptions):
88 if self._busyReason is not None:
89 raise RuntimeError("Please wait for %r" % self._busyReason)
90 if contactId in self._contacts:
91 _moduleLogger.info("Adding duplicate contact %r" % contactId)
92 # @todo Remove this evil hack to re-popup the dialog
93 self.recipientsChanged.emit()
95 contactDetails = _DraftContact(title, description, numbersWithDescriptions)
96 self._contacts[contactId] = contactDetails
97 self.recipientsChanged.emit()
99 def remove_contact(self, contactId):
100 if self._busyReason is not None:
101 raise RuntimeError("Please wait for %r" % self._busyReason)
102 assert contactId in self._contacts, "Contact missing"
103 del self._contacts[contactId]
104 self.recipientsChanged.emit()
106 def get_contacts(self):
107 return self._contacts.iterkeys()
109 def get_num_contacts(self):
110 return len(self._contacts)
112 def get_title(self, cid):
113 return self._contacts[cid].title
115 def get_description(self, cid):
116 return self._contacts[cid].description
118 def get_numbers(self, cid):
119 return self._contacts[cid].numbers
121 def get_selected_number(self, cid):
122 return self._contacts[cid].selectedNumber
124 def set_selected_number(self, cid, number):
125 # @note I'm lazy, this isn't firing any kind of signal since only one
126 # controller right now and that is the viewer
127 assert number in (nWD[0] for nWD in self._contacts[cid].numbers), "Number not selectable"
128 self._contacts[cid].selectedNumber = number
131 if self._busyReason is not None:
132 raise RuntimeError("Please wait for %r" % self._busyReason)
136 oldContacts = self._contacts
140 self.recipientsChanged.emit()
142 @contextlib.contextmanager
143 def _busy(self, message):
144 if self._busyReason is not None:
145 raise RuntimeError("Already busy doing %r" % self._busyReason)
147 self._busyReason = message
150 self._busyReason = None
152 def _send(self, numbers, text):
153 self.sendingMessage.emit()
155 with self._busy("Sending Text"):
156 with qui_utils.notify_busy(self._errorLog, "Sending Text"):
158 self._backend[0].send_sms,
162 self.sentMessage.emit()
165 self.error.emit(str(e))
167 def _call(self, number):
170 with self._busy("Calling"):
171 with qui_utils.notify_busy(self._errorLog, "Calling"):
173 self._backend[0].call,
180 self.error.emit(str(e))
183 self.cancelling.emit()
185 with qui_utils.notify_busy(self._errorLog, "Cancelling"):
187 self._backend[0].cancel,
191 self.cancelled.emit()
193 self.error.emit(str(e))
196 class Session(QtCore.QObject):
198 # @todo Somehow add support for csv contacts
200 stateChange = QtCore.pyqtSignal(str)
201 loggedOut = QtCore.pyqtSignal()
202 loggedIn = QtCore.pyqtSignal()
203 callbackNumberChanged = QtCore.pyqtSignal(str)
205 contactsUpdated = QtCore.pyqtSignal()
206 messagesUpdated = QtCore.pyqtSignal()
207 historyUpdated = QtCore.pyqtSignal()
208 dndStateChange = QtCore.pyqtSignal(bool)
210 error = QtCore.pyqtSignal(str)
212 LOGGEDOUT_STATE = "logged out"
213 LOGGINGIN_STATE = "logging in"
214 LOGGEDIN_STATE = "logged in"
216 _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.1.90")
221 def __init__(self, errorLog, cachePath = None):
222 QtCore.QObject.__init__(self)
223 self._errorLog = errorLog
224 self._pool = qore_utils.AsyncPool()
226 self._loggedInTime = self._LOGGEDOUT_TIME
228 self._cachePath = cachePath
229 self._username = None
230 self._draft = Draft(self._pool, self._backend, self._errorLog)
233 self._contactUpdateTime = datetime.datetime(1971, 1, 1)
235 self._messageUpdateTime = datetime.datetime(1971, 1, 1)
237 self._historyUpdateTime = datetime.datetime(1971, 1, 1)
244 self._LOGGEDOUT_TIME: self.LOGGEDOUT_STATE,
245 self._LOGGINGIN_TIME: self.LOGGINGIN_STATE,
246 }.get(self._loggedInTime, self.LOGGEDIN_STATE)
252 def login(self, username, password):
253 assert self.state == self.LOGGEDOUT_STATE, "Can only log-in when logged out (currently %s" % self.state
254 assert username != "", "No username specified"
255 if self._cachePath is not None:
256 cookiePath = os.path.join(self._cachePath, "%s.cookies" % username)
260 if self._username != username or not self._backend:
261 from backends import gv_backend
263 self._backend[0:0] = [gv_backend.GVDialer(cookiePath)]
266 le = concurrent.AsyncLinearExecution(self._pool, self._login)
267 le.start(username, password)
270 assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state
272 self._loggedInTime = self._LOGGEDOUT_TIME
273 self._backend[0].persist()
274 self._save_to_cache()
277 assert self.state == self.LOGGEDOUT_STATE, "Can only clear when logged out (currently %s" % self.state
278 self._backend[0].logout()
283 def logout_and_clear(self):
284 assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state
286 self._loggedInTime = self._LOGGEDOUT_TIME
289 def update_contacts(self, force = True):
290 if not force and self._contacts:
292 le = concurrent.AsyncLinearExecution(self._pool, self._update_contacts)
293 self._perform_op_while_loggedin(le)
295 def get_contacts(self):
296 return self._contacts
298 def get_when_contacts_updated(self):
299 return self._contactUpdateTime
301 def update_messages(self, force = True):
302 if not force and self._messages:
304 le = concurrent.AsyncLinearExecution(self._pool, self._update_messages)
305 self._perform_op_while_loggedin(le)
307 def get_messages(self):
308 return self._messages
310 def get_when_messages_updated(self):
311 return self._messageUpdateTime
313 def update_history(self, force = True):
314 if not force and self._history:
316 le = concurrent.AsyncLinearExecution(self._pool, self._update_history)
317 self._perform_op_while_loggedin(le)
319 def get_history(self):
322 def get_when_history_updated(self):
323 return self._historyUpdateTime
325 def update_dnd(self):
326 le = concurrent.AsyncLinearExecution(self._pool, self._update_dnd)
327 self._perform_op_while_loggedin(le)
329 def set_dnd(self, dnd):
330 le = concurrent.AsyncLinearExecution(self._pool, self._set_dnd)
333 def _set_dnd(self, dnd):
334 # I'm paranoid about our state geting out of sync so we set no matter
335 # what but act as if we have the cannonical state
336 assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state
339 with qui_utils.notify_busy(self._errorLog, "Setting DND Status"):
341 self._backend[0].set_dnd,
346 self.error.emit(str(e))
349 if oldDnd != self._dnd:
350 self.dndStateChange.emit(self._dnd)
355 def get_account_number(self):
356 return self._backend[0].get_account_number()
358 def get_callback_numbers(self):
359 # @todo Remove evilness (might call is_authed which can block)
360 return self._backend[0].get_callback_numbers()
362 def get_callback_number(self):
363 return self._callback
365 def set_callback_number(self, callback):
366 le = concurrent.AsyncLinearExecution(self._pool, self._set_callback_number)
369 def _set_callback_number(self, callback):
370 # I'm paranoid about our state geting out of sync so we set no matter
371 # what but act as if we have the cannonical state
372 assert self.state == self.LOGGEDIN_STATE, "Callbacks configurable only when logged in (currently %s" % self.state
373 oldCallback = self._callback
376 self._backend[0].set_callback_number,
381 self.error.emit(str(e))
383 self._callback = callback
384 if oldCallback != self._callback:
385 self.callbackNumberChanged.emit(self._callback)
387 def _login(self, username, password):
388 with qui_utils.notify_busy(self._errorLog, "Logging In"):
389 self._loggedInTime = self._LOGGINGIN_TIME
390 self.stateChange.emit(self.LOGGINGIN_STATE)
391 finalState = self.LOGGEDOUT_STATE
394 if not isLoggedIn and self._backend[0].is_quick_login_possible():
396 self._backend[0].is_authed,
401 _moduleLogger.info("Logged in through cookies")
403 # Force a clearing of the cookies
405 self._backend[0].logout,
412 self._backend[0].login,
413 (username, password),
417 _moduleLogger.info("Logged in through credentials")
420 self._loggedInTime = int(time.time())
421 oldUsername = self._username
422 self._username = username
423 finalState = self.LOGGEDIN_STATE
424 if oldUsername != self._username:
425 needOps = not self._load()
432 loginOps = self._loginOps[:]
435 del self._loginOps[:]
436 for asyncOp in loginOps:
439 self._loggedInTime = self._LOGGEDOUT_TIME
440 self.error.emit("Error logging in")
442 self._loggedInTime = self._LOGGEDOUT_TIME
443 self.error.emit(str(e))
445 self.stateChange.emit(finalState)
446 if isLoggedIn and self._callback:
447 self.set_callback_number(self._callback)
450 updateContacts = len(self._contacts) != 0
451 updateMessages = len(self._messages) != 0
452 updateHistory = len(self._history) != 0
454 oldCallback = self._callback
462 loadedFromCache = self._load_from_cache()
464 updateContacts = True
465 updateMessages = True
469 self.contactsUpdated.emit()
471 self.messagesUpdated.emit()
473 self.historyUpdated.emit()
474 if oldDnd != self._dnd:
475 self.dndStateChange.emit(self._dnd)
476 if oldCallback != self._callback:
477 self.callbackNumberChanged.emit(self._callback)
479 return loadedFromCache
481 def _load_from_cache(self):
482 if self._cachePath is None:
484 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
487 with open(cachePath, "rb") as f:
488 dumpedData = pickle.load(f)
489 except (pickle.PickleError, IOError, EOFError, ValueError):
490 _moduleLogger.exception("Pickle fun loading")
493 _moduleLogger.exception("Weirdness loading")
498 contacts, contactUpdateTime,
499 messages, messageUpdateTime,
500 history, historyUpdateTime,
504 if misc_utils.compare_versions(
505 self._OLDEST_COMPATIBLE_FORMAT_VERSION,
506 misc_utils.parse_version(version),
508 _moduleLogger.info("Loaded cache")
509 self._contacts = contacts
510 self._contactUpdateTime = contactUpdateTime
511 self._messages = messages
512 self._messageUpdateTime = messageUpdateTime
513 self._history = history
514 self._historyUpdateTime = historyUpdateTime
516 self._callback = callback
520 "Skipping cache due to version mismatch (%s-%s)" % (
526 def _save_to_cache(self):
527 _moduleLogger.info("Saving cache")
528 if self._cachePath is None:
530 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
534 constants.__version__, constants.__build__,
535 self._contacts, self._contactUpdateTime,
536 self._messages, self._messageUpdateTime,
537 self._history, self._historyUpdateTime,
538 self._dnd, self._callback
540 with open(cachePath, "wb") as f:
541 pickle.dump(dataToDump, f, pickle.HIGHEST_PROTOCOL)
542 _moduleLogger.info("Cache saved")
543 except (pickle.PickleError, IOError):
544 _moduleLogger.exception("While saving")
546 def _clear_cache(self):
547 updateContacts = len(self._contacts) != 0
548 updateMessages = len(self._messages) != 0
549 updateHistory = len(self._history) != 0
551 oldCallback = self._callback
554 self._contactUpdateTime = datetime.datetime(1, 1, 1)
556 self._messageUpdateTime = datetime.datetime(1, 1, 1)
558 self._historyUpdateTime = datetime.datetime(1, 1, 1)
563 self.contactsUpdated.emit()
565 self.messagesUpdated.emit()
567 self.historyUpdated.emit()
568 if oldDnd != self._dnd:
569 self.dndStateChange.emit(self._dnd)
570 if oldCallback != self._callback:
571 self.callbackNumberChanged.emit(self._callback)
573 self._save_to_cache()
575 def _update_contacts(self):
577 with qui_utils.notify_busy(self._errorLog, "Updating Contacts"):
578 self._contacts = yield (
579 self._backend[0].get_contacts,
584 self.error.emit(str(e))
586 self._contactUpdateTime = datetime.datetime.now()
587 self.contactsUpdated.emit()
589 def _update_messages(self):
591 with qui_utils.notify_busy(self._errorLog, "Updating Messages"):
592 self._messages = yield (
593 self._backend[0].get_messages,
598 self.error.emit(str(e))
600 self._messageUpdateTime = datetime.datetime.now()
601 self.messagesUpdated.emit()
603 def _update_history(self):
605 with qui_utils.notify_busy(self._errorLog, "Updating History"):
606 self._history = yield (
607 self._backend[0].get_recent,
612 self.error.emit(str(e))
614 self._historyUpdateTime = datetime.datetime.now()
615 self.historyUpdated.emit()
617 def _update_dnd(self):
621 self._backend[0].is_dnd,
626 self.error.emit(str(e))
628 if oldDnd != self._dnd:
629 self.dndStateChange(self._dnd)
631 def _perform_op_while_loggedin(self, op):
632 if self.state == self.LOGGEDIN_STATE:
635 self._push_login_op(op)
637 def _push_login_op(self, asyncOp):
638 assert self.state != self.LOGGEDIN_STATE, "Can only queue work when logged out"
639 if asyncOp in self._loginOps:
640 _moduleLogger.info("Skipping queueing duplicate op: %r" % asyncOp)
642 self._loginOps.append(asyncOp)