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 self._message = message
83 message = property(_get_message, _set_message)
85 def add_contact(self, contactId, title, description, numbersWithDescriptions):
86 if self._busyReason is not None:
87 raise RuntimeError("Please wait for %r" % self._busyReason)
88 if contactId in self._contacts:
89 _moduleLogger.info("Adding duplicate contact %r" % contactId)
90 # @todo Remove this evil hack to re-popup the dialog
91 self.recipientsChanged.emit()
93 contactDetails = _DraftContact(title, description, numbersWithDescriptions)
94 self._contacts[contactId] = contactDetails
95 self.recipientsChanged.emit()
97 def remove_contact(self, contactId):
98 if self._busyReason is not None:
99 raise RuntimeError("Please wait for %r" % self._busyReason)
100 assert contactId in self._contacts, "Contact missing"
101 del self._contacts[contactId]
102 self.recipientsChanged.emit()
104 def get_contacts(self):
105 return self._contacts.iterkeys()
107 def get_num_contacts(self):
108 return len(self._contacts)
110 def get_title(self, cid):
111 return self._contacts[cid].title
113 def get_description(self, cid):
114 return self._contacts[cid].description
116 def get_numbers(self, cid):
117 return self._contacts[cid].numbers
119 def get_selected_number(self, cid):
120 return self._contacts[cid].selectedNumber
122 def set_selected_number(self, cid, number):
123 # @note I'm lazy, this isn't firing any kind of signal since only one
124 # controller right now and that is the viewer
125 assert number in (nWD[0] for nWD in self._contacts[cid].numbers), "Number not selectable"
126 self._contacts[cid].selectedNumber = number
129 if self._busyReason is not None:
130 raise RuntimeError("Please wait for %r" % self._busyReason)
134 oldContacts = self._contacts
138 self.recipientsChanged.emit()
140 @contextlib.contextmanager
141 def _busy(self, message):
142 if self._busyReason is not None:
143 raise RuntimeError("Already busy doing %r" % self._busyReason)
145 self._busyReason = message
148 self._busyReason = None
150 def _send(self, numbers, text):
151 self.sendingMessage.emit()
153 with self._busy("Sending Text"):
154 with qui_utils.notify_busy(self._errorLog, "Sending Text"):
156 self._backend[0].send_sms,
160 self.sentMessage.emit()
163 _moduleLogger.exception("Reporting error to user")
164 self.error.emit(str(e))
166 def _call(self, number):
169 with self._busy("Calling"):
170 with qui_utils.notify_busy(self._errorLog, "Calling"):
172 self._backend[0].call,
179 _moduleLogger.exception("Reporting error to user")
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 _moduleLogger.exception("Reporting error to user")
194 self.error.emit(str(e))
197 class Session(QtCore.QObject):
199 # @todo Somehow add support for csv contacts
201 stateChange = QtCore.pyqtSignal(str)
202 loggedOut = QtCore.pyqtSignal()
203 loggedIn = QtCore.pyqtSignal()
204 callbackNumberChanged = QtCore.pyqtSignal(str)
206 contactsUpdated = QtCore.pyqtSignal()
207 messagesUpdated = QtCore.pyqtSignal()
208 historyUpdated = QtCore.pyqtSignal()
209 dndStateChange = QtCore.pyqtSignal(bool)
211 error = QtCore.pyqtSignal(str)
213 LOGGEDOUT_STATE = "logged out"
214 LOGGINGIN_STATE = "logging in"
215 LOGGEDIN_STATE = "logged in"
217 _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.1.90")
222 def __init__(self, errorLog, cachePath = None):
223 QtCore.QObject.__init__(self)
224 self._errorLog = errorLog
225 self._pool = qore_utils.AsyncPool()
227 self._loggedInTime = self._LOGGEDOUT_TIME
229 self._cachePath = cachePath
230 self._username = None
231 self._draft = Draft(self._pool, self._backend, self._errorLog)
234 self._contactUpdateTime = datetime.datetime(1971, 1, 1)
236 self._messageUpdateTime = datetime.datetime(1971, 1, 1)
238 self._historyUpdateTime = datetime.datetime(1971, 1, 1)
245 self._LOGGEDOUT_TIME: self.LOGGEDOUT_STATE,
246 self._LOGGINGIN_TIME: self.LOGGINGIN_STATE,
247 }.get(self._loggedInTime, self.LOGGEDIN_STATE)
253 def login(self, username, password):
254 assert self.state == self.LOGGEDOUT_STATE, "Can only log-in when logged out (currently %s" % self.state
255 assert username != "", "No username specified"
256 if self._cachePath is not None:
257 cookiePath = os.path.join(self._cachePath, "%s.cookies" % username)
261 if self._username != username or not self._backend:
262 from backends import gv_backend
264 self._backend[0:0] = [gv_backend.GVDialer(cookiePath)]
267 le = concurrent.AsyncLinearExecution(self._pool, self._login)
268 le.start(username, password)
271 assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state
272 _moduleLogger.info("Logging out")
274 self._loggedInTime = self._LOGGEDOUT_TIME
275 self._backend[0].persist()
276 self._save_to_cache()
277 self.stateChange.emit(self.LOGGEDOUT_STATE)
278 self.loggedOut.emit()
281 assert self.state == self.LOGGEDOUT_STATE, "Can only clear when logged out (currently %s" % self.state
282 self._backend[0].logout()
287 def logout_and_clear(self):
288 assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state
289 _moduleLogger.info("Logging out and clearing the account")
291 self._loggedInTime = self._LOGGEDOUT_TIME
293 self.stateChange.emit(self.LOGGEDOUT_STATE)
294 self.loggedOut.emit()
296 def update_contacts(self, force = True):
297 if not force and self._contacts:
299 le = concurrent.AsyncLinearExecution(self._pool, self._update_contacts)
300 self._perform_op_while_loggedin(le)
302 def get_contacts(self):
303 return self._contacts
305 def get_when_contacts_updated(self):
306 return self._contactUpdateTime
308 def update_messages(self, force = True):
309 if not force and self._messages:
311 le = concurrent.AsyncLinearExecution(self._pool, self._update_messages)
312 self._perform_op_while_loggedin(le)
314 def get_messages(self):
315 return self._messages
317 def get_when_messages_updated(self):
318 return self._messageUpdateTime
320 def update_history(self, force = True):
321 if not force and self._history:
323 le = concurrent.AsyncLinearExecution(self._pool, self._update_history)
324 self._perform_op_while_loggedin(le)
326 def get_history(self):
329 def get_when_history_updated(self):
330 return self._historyUpdateTime
332 def update_dnd(self):
333 le = concurrent.AsyncLinearExecution(self._pool, self._update_dnd)
334 self._perform_op_while_loggedin(le)
336 def set_dnd(self, dnd):
337 le = concurrent.AsyncLinearExecution(self._pool, self._set_dnd)
340 def _set_dnd(self, dnd):
343 assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state
344 with qui_utils.notify_busy(self._errorLog, "Setting DND Status"):
346 self._backend[0].set_dnd,
351 _moduleLogger.exception("Reporting error to user")
352 self.error.emit(str(e))
355 if oldDnd != self._dnd:
356 self.dndStateChange.emit(self._dnd)
361 def get_account_number(self):
362 if self.state != self.LOGGEDIN_STATE:
364 return self._backend[0].get_account_number()
366 def get_callback_numbers(self):
367 if self.state != self.LOGGEDIN_STATE:
369 return self._backend[0].get_callback_numbers()
371 def get_callback_number(self):
372 return self._callback
374 def set_callback_number(self, callback):
375 le = concurrent.AsyncLinearExecution(self._pool, self._set_callback_number)
378 def _set_callback_number(self, callback):
379 oldCallback = self._callback
381 assert self.state == self.LOGGEDIN_STATE, "Callbacks configurable only when logged in (currently %s" % self.state
383 self._backend[0].set_callback_number,
388 _moduleLogger.exception("Reporting error to user")
389 self.error.emit(str(e))
391 self._callback = callback
392 if oldCallback != self._callback:
393 self.callbackNumberChanged.emit(self._callback)
395 def _login(self, username, password):
396 with qui_utils.notify_busy(self._errorLog, "Logging In"):
397 self._loggedInTime = self._LOGGINGIN_TIME
398 self.stateChange.emit(self.LOGGINGIN_STATE)
399 finalState = self.LOGGEDOUT_STATE
402 if not isLoggedIn and self._backend[0].is_quick_login_possible():
404 self._backend[0].is_authed,
409 _moduleLogger.info("Logged in through cookies")
411 # Force a clearing of the cookies
413 self._backend[0].logout,
420 self._backend[0].login,
421 (username, password),
425 _moduleLogger.info("Logged in through credentials")
428 self._loggedInTime = int(time.time())
429 oldUsername = self._username
430 self._username = username
431 finalState = self.LOGGEDIN_STATE
432 if oldUsername != self._username:
433 needOps = not self._load()
438 self.stateChange.emit(finalState)
439 finalState = None # Mark it as already set
442 loginOps = self._loginOps[:]
445 del self._loginOps[:]
446 for asyncOp in loginOps:
449 self._loggedInTime = self._LOGGEDOUT_TIME
450 self.error.emit("Error logging in")
452 self._loggedInTime = self._LOGGEDOUT_TIME
453 _moduleLogger.exception("Reporting error to user")
454 self.error.emit(str(e))
456 if finalState is not None:
457 self.stateChange.emit(finalState)
458 if isLoggedIn and self._callback:
459 self.set_callback_number(self._callback)
462 updateContacts = len(self._contacts) != 0
463 updateMessages = len(self._messages) != 0
464 updateHistory = len(self._history) != 0
466 oldCallback = self._callback
474 loadedFromCache = self._load_from_cache()
476 updateContacts = True
477 updateMessages = True
481 self.contactsUpdated.emit()
483 self.messagesUpdated.emit()
485 self.historyUpdated.emit()
486 if oldDnd != self._dnd:
487 self.dndStateChange.emit(self._dnd)
488 if oldCallback != self._callback:
489 self.callbackNumberChanged.emit(self._callback)
491 return loadedFromCache
493 def _load_from_cache(self):
494 if self._cachePath is None:
496 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
499 with open(cachePath, "rb") as f:
500 dumpedData = pickle.load(f)
501 except (pickle.PickleError, IOError, EOFError, ValueError):
502 _moduleLogger.exception("Pickle fun loading")
505 _moduleLogger.exception("Weirdness loading")
511 contacts, contactUpdateTime,
512 messages, messageUpdateTime,
513 history, historyUpdateTime,
517 _moduleLogger.exception("Upgrade/downgrade fun")
520 _moduleLogger.exception("Weirdlings")
522 if misc_utils.compare_versions(
523 self._OLDEST_COMPATIBLE_FORMAT_VERSION,
524 misc_utils.parse_version(version),
526 _moduleLogger.info("Loaded cache")
527 self._contacts = contacts
528 self._contactUpdateTime = contactUpdateTime
529 self._messages = messages
530 self._messageUpdateTime = messageUpdateTime
531 self._history = history
532 self._historyUpdateTime = historyUpdateTime
534 self._callback = callback
538 "Skipping cache due to version mismatch (%s-%s)" % (
544 def _save_to_cache(self):
545 _moduleLogger.info("Saving cache")
546 if self._cachePath is None:
548 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
552 constants.__version__, constants.__build__,
553 self._contacts, self._contactUpdateTime,
554 self._messages, self._messageUpdateTime,
555 self._history, self._historyUpdateTime,
556 self._dnd, self._callback
558 with open(cachePath, "wb") as f:
559 pickle.dump(dataToDump, f, pickle.HIGHEST_PROTOCOL)
560 _moduleLogger.info("Cache saved")
561 except (pickle.PickleError, IOError):
562 _moduleLogger.exception("While saving")
564 def _clear_cache(self):
565 updateContacts = len(self._contacts) != 0
566 updateMessages = len(self._messages) != 0
567 updateHistory = len(self._history) != 0
569 oldCallback = self._callback
572 self._contactUpdateTime = datetime.datetime(1971, 1, 1)
574 self._messageUpdateTime = datetime.datetime(1971, 1, 1)
576 self._historyUpdateTime = datetime.datetime(1971, 1, 1)
581 self.contactsUpdated.emit()
583 self.messagesUpdated.emit()
585 self.historyUpdated.emit()
586 if oldDnd != self._dnd:
587 self.dndStateChange.emit(self._dnd)
588 if oldCallback != self._callback:
589 self.callbackNumberChanged.emit(self._callback)
591 self._save_to_cache()
593 def _update_contacts(self):
595 assert self.state == self.LOGGEDIN_STATE, "Contacts requires being logged in (currently %s" % self.state
596 with qui_utils.notify_busy(self._errorLog, "Updating Contacts"):
597 self._contacts = yield (
598 self._backend[0].get_contacts,
603 _moduleLogger.exception("Reporting error to user")
604 self.error.emit(str(e))
606 self._contactUpdateTime = datetime.datetime.now()
607 self.contactsUpdated.emit()
609 def _update_messages(self):
611 assert self.state == self.LOGGEDIN_STATE, "Messages requires being logged in (currently %s" % self.state
612 with qui_utils.notify_busy(self._errorLog, "Updating Messages"):
613 self._messages = yield (
614 self._backend[0].get_messages,
619 _moduleLogger.exception("Reporting error to user")
620 self.error.emit(str(e))
622 self._messageUpdateTime = datetime.datetime.now()
623 self.messagesUpdated.emit()
625 def _update_history(self):
627 assert self.state == self.LOGGEDIN_STATE, "History requires being logged in (currently %s" % self.state
628 with qui_utils.notify_busy(self._errorLog, "Updating History"):
629 self._history = yield (
630 self._backend[0].get_recent,
635 _moduleLogger.exception("Reporting error to user")
636 self.error.emit(str(e))
638 self._historyUpdateTime = datetime.datetime.now()
639 self.historyUpdated.emit()
641 def _update_dnd(self):
644 assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state
646 self._backend[0].is_dnd,
651 _moduleLogger.exception("Reporting error to user")
652 self.error.emit(str(e))
654 if oldDnd != self._dnd:
655 self.dndStateChange(self._dnd)
657 def _perform_op_while_loggedin(self, op):
658 if self.state == self.LOGGEDIN_STATE:
661 self._push_login_op(op)
663 def _push_login_op(self, asyncOp):
664 assert self.state != self.LOGGEDIN_STATE, "Can only queue work when logged out"
665 if asyncOp in self._loginOps:
666 _moduleLogger.info("Skipping queueing duplicate op: %r" % asyncOp)
668 self._loginOps.append(asyncOp)