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 # Allow overwriting of contacts so that the message can be updated and the SMS dialog popped back up
89 contactDetails = _DraftContact(title, description, numbersWithDescriptions)
90 self._contacts[contactId] = contactDetails
91 self.recipientsChanged.emit()
93 def remove_contact(self, contactId):
94 if self._busyReason is not None:
95 raise RuntimeError("Please wait for %r" % self._busyReason)
96 assert contactId in self._contacts, "Contact missing"
97 del self._contacts[contactId]
98 self.recipientsChanged.emit()
100 def get_contacts(self):
101 return self._contacts.iterkeys()
103 def get_num_contacts(self):
104 return len(self._contacts)
106 def get_title(self, cid):
107 return self._contacts[cid].title
109 def get_description(self, cid):
110 return self._contacts[cid].description
112 def get_numbers(self, cid):
113 return self._contacts[cid].numbers
115 def get_selected_number(self, cid):
116 return self._contacts[cid].selectedNumber
118 def set_selected_number(self, cid, number):
119 # @note I'm lazy, this isn't firing any kind of signal since only one
120 # controller right now and that is the viewer
121 assert number in (nWD[0] for nWD in self._contacts[cid].numbers), "Number not selectable"
122 self._contacts[cid].selectedNumber = number
125 if self._busyReason is not None:
126 raise RuntimeError("Please wait for %r" % self._busyReason)
130 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 qui_utils.notify_busy(self._errorLog, "Sending Text"):
152 self._backend[0].send_sms,
156 self.sentMessage.emit()
159 _moduleLogger.exception("Reporting error to user")
160 self.error.emit(str(e))
162 def _call(self, number):
165 with self._busy("Calling"):
166 with qui_utils.notify_busy(self._errorLog, "Calling"):
168 self._backend[0].call,
175 _moduleLogger.exception("Reporting error to user")
176 self.error.emit(str(e))
179 self.cancelling.emit()
181 with qui_utils.notify_busy(self._errorLog, "Cancelling"):
183 self._backend[0].cancel,
187 self.cancelled.emit()
189 _moduleLogger.exception("Reporting error to user")
190 self.error.emit(str(e))
193 class Session(QtCore.QObject):
195 # @todo Somehow add support for csv contacts
197 stateChange = QtCore.pyqtSignal(str)
198 loggedOut = QtCore.pyqtSignal()
199 loggedIn = QtCore.pyqtSignal()
200 callbackNumberChanged = QtCore.pyqtSignal(str)
202 contactsUpdated = QtCore.pyqtSignal()
203 messagesUpdated = QtCore.pyqtSignal()
204 newMessages = QtCore.pyqtSignal()
205 historyUpdated = QtCore.pyqtSignal()
206 dndStateChange = QtCore.pyqtSignal(bool)
208 error = QtCore.pyqtSignal(str)
210 LOGGEDOUT_STATE = "logged out"
211 LOGGINGIN_STATE = "logging in"
212 LOGGEDIN_STATE = "logged in"
214 _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.1.90")
219 def __init__(self, errorLog, cachePath = None):
220 QtCore.QObject.__init__(self)
221 self._errorLog = errorLog
222 self._pool = qore_utils.AsyncPool()
224 self._loggedInTime = self._LOGGEDOUT_TIME
226 self._cachePath = cachePath
227 self._username = None
228 self._draft = Draft(self._pool, self._backend, self._errorLog)
231 self._contactUpdateTime = datetime.datetime(1971, 1, 1)
233 self._cleanMessages = []
234 self._messageUpdateTime = datetime.datetime(1971, 1, 1)
236 self._historyUpdateTime = datetime.datetime(1971, 1, 1)
243 self._LOGGEDOUT_TIME: self.LOGGEDOUT_STATE,
244 self._LOGGINGIN_TIME: self.LOGGINGIN_STATE,
245 }.get(self._loggedInTime, self.LOGGEDIN_STATE)
251 def login(self, username, password):
252 assert self.state == self.LOGGEDOUT_STATE, "Can only log-in when logged out (currently %s" % self.state
253 assert username != "", "No username specified"
254 if self._cachePath is not None:
255 cookiePath = os.path.join(self._cachePath, "%s.cookies" % username)
259 if self._username != username or not self._backend:
260 from backends import gv_backend
262 self._backend[0:0] = [gv_backend.GVDialer(cookiePath)]
265 le = concurrent.AsyncLinearExecution(self._pool, self._login)
266 le.start(username, password)
269 assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state
270 _moduleLogger.info("Logging out")
272 self._loggedInTime = self._LOGGEDOUT_TIME
273 self._backend[0].persist()
274 self._save_to_cache()
275 self.stateChange.emit(self.LOGGEDOUT_STATE)
276 self.loggedOut.emit()
279 assert self.state == self.LOGGEDOUT_STATE, "Can only clear when logged out (currently %s" % self.state
280 self._backend[0].logout()
285 def logout_and_clear(self):
286 assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state
287 _moduleLogger.info("Logging out and clearing the account")
289 self._loggedInTime = self._LOGGEDOUT_TIME
291 self.stateChange.emit(self.LOGGEDOUT_STATE)
292 self.loggedOut.emit()
294 def update_contacts(self, force = True):
295 if not force and self._contacts:
297 le = concurrent.AsyncLinearExecution(self._pool, self._update_contacts)
298 self._perform_op_while_loggedin(le)
300 def get_contacts(self):
301 return self._contacts
303 def get_when_contacts_updated(self):
304 return self._contactUpdateTime
306 def update_messages(self, force = True):
307 if not force and self._messages:
309 le = concurrent.AsyncLinearExecution(self._pool, self._update_messages)
310 self._perform_op_while_loggedin(le)
312 def get_messages(self):
313 return self._messages
315 def get_when_messages_updated(self):
316 return self._messageUpdateTime
318 def update_history(self, force = True):
319 if not force and self._history:
321 le = concurrent.AsyncLinearExecution(self._pool, self._update_history)
322 self._perform_op_while_loggedin(le)
324 def get_history(self):
327 def get_when_history_updated(self):
328 return self._historyUpdateTime
330 def update_dnd(self):
331 le = concurrent.AsyncLinearExecution(self._pool, self._update_dnd)
332 self._perform_op_while_loggedin(le)
334 def set_dnd(self, dnd):
335 le = concurrent.AsyncLinearExecution(self._pool, self._set_dnd)
338 def _set_dnd(self, dnd):
341 assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state
342 with qui_utils.notify_busy(self._errorLog, "Setting DND Status"):
344 self._backend[0].set_dnd,
349 _moduleLogger.exception("Reporting error to user")
350 self.error.emit(str(e))
353 if oldDnd != self._dnd:
354 self.dndStateChange.emit(self._dnd)
359 def get_account_number(self):
360 if self.state != self.LOGGEDIN_STATE:
362 return self._backend[0].get_account_number()
364 def get_callback_numbers(self):
365 if self.state != self.LOGGEDIN_STATE:
367 return self._backend[0].get_callback_numbers()
369 def get_callback_number(self):
370 return self._callback
372 def set_callback_number(self, callback):
373 le = concurrent.AsyncLinearExecution(self._pool, self._set_callback_number)
376 def _set_callback_number(self, callback):
377 oldCallback = self._callback
379 assert self.state == self.LOGGEDIN_STATE, "Callbacks configurable only when logged in (currently %s" % self.state
381 self._backend[0].set_callback_number,
386 _moduleLogger.exception("Reporting error to user")
387 self.error.emit(str(e))
389 self._callback = callback
390 if oldCallback != self._callback:
391 self.callbackNumberChanged.emit(self._callback)
393 def _login(self, username, password):
394 with qui_utils.notify_busy(self._errorLog, "Logging In"):
395 self._loggedInTime = self._LOGGINGIN_TIME
396 self.stateChange.emit(self.LOGGINGIN_STATE)
397 finalState = self.LOGGEDOUT_STATE
400 if not isLoggedIn and self._backend[0].is_quick_login_possible():
402 self._backend[0].is_authed,
407 _moduleLogger.info("Logged in through cookies")
409 # Force a clearing of the cookies
411 self._backend[0].logout,
418 self._backend[0].login,
419 (username, password),
423 _moduleLogger.info("Logged in through credentials")
426 self._loggedInTime = int(time.time())
427 oldUsername = self._username
428 self._username = username
429 finalState = self.LOGGEDIN_STATE
430 if oldUsername != self._username:
431 needOps = not self._load()
436 self.stateChange.emit(finalState)
437 finalState = None # Mark it as already set
440 loginOps = self._loginOps[:]
443 del self._loginOps[:]
444 for asyncOp in loginOps:
447 self._loggedInTime = self._LOGGEDOUT_TIME
448 self.error.emit("Error logging in")
450 _moduleLogger.exception("Booh")
451 self._loggedInTime = self._LOGGEDOUT_TIME
452 _moduleLogger.exception("Reporting error to user")
453 self.error.emit(str(e))
455 if finalState is not None:
456 self.stateChange.emit(finalState)
457 if isLoggedIn and self._callback:
458 self.set_callback_number(self._callback)
461 updateContacts = len(self._contacts) != 0
462 updateMessages = len(self._messages) != 0
463 updateHistory = len(self._history) != 0
465 oldCallback = self._callback
469 self._cleanMessages = []
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, ImportError):
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._alert_on_messages(self._messages)
531 self._messageUpdateTime = messageUpdateTime
532 self._history = history
533 self._historyUpdateTime = historyUpdateTime
535 self._callback = callback
539 "Skipping cache due to version mismatch (%s-%s)" % (
545 def _save_to_cache(self):
546 _moduleLogger.info("Saving cache")
547 if self._cachePath is None:
549 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
553 constants.__version__, constants.__build__,
554 self._contacts, self._contactUpdateTime,
555 self._messages, self._messageUpdateTime,
556 self._history, self._historyUpdateTime,
557 self._dnd, self._callback
559 with open(cachePath, "wb") as f:
560 pickle.dump(dataToDump, f, pickle.HIGHEST_PROTOCOL)
561 _moduleLogger.info("Cache saved")
562 except (pickle.PickleError, IOError):
563 _moduleLogger.exception("While saving")
565 def _clear_cache(self):
566 updateContacts = len(self._contacts) != 0
567 updateMessages = len(self._messages) != 0
568 updateHistory = len(self._history) != 0
570 oldCallback = self._callback
573 self._contactUpdateTime = datetime.datetime(1971, 1, 1)
575 self._messageUpdateTime = datetime.datetime(1971, 1, 1)
577 self._historyUpdateTime = datetime.datetime(1971, 1, 1)
582 self.contactsUpdated.emit()
584 self.messagesUpdated.emit()
586 self.historyUpdated.emit()
587 if oldDnd != self._dnd:
588 self.dndStateChange.emit(self._dnd)
589 if oldCallback != self._callback:
590 self.callbackNumberChanged.emit(self._callback)
592 self._save_to_cache()
594 def _update_contacts(self):
596 assert self.state == self.LOGGEDIN_STATE, "Contacts requires being logged in (currently %s" % self.state
597 with qui_utils.notify_busy(self._errorLog, "Updating Contacts"):
598 self._contacts = yield (
599 self._backend[0].get_contacts,
604 _moduleLogger.exception("Reporting error to user")
605 self.error.emit(str(e))
607 self._contactUpdateTime = datetime.datetime.now()
608 self.contactsUpdated.emit()
610 def _update_messages(self):
612 assert self.state == self.LOGGEDIN_STATE, "Messages requires being logged in (currently %s" % self.state
613 with qui_utils.notify_busy(self._errorLog, "Updating Messages"):
614 self._messages = yield (
615 self._backend[0].get_messages,
620 _moduleLogger.exception("Reporting error to user")
621 self.error.emit(str(e))
623 self._messageUpdateTime = datetime.datetime.now()
624 self.messagesUpdated.emit()
625 self._alert_on_messages(self._messages)
627 def _update_history(self):
629 assert self.state == self.LOGGEDIN_STATE, "History requires being logged in (currently %s" % self.state
630 with qui_utils.notify_busy(self._errorLog, "Updating History"):
631 self._history = yield (
632 self._backend[0].get_recent,
637 _moduleLogger.exception("Reporting error to user")
638 self.error.emit(str(e))
640 self._historyUpdateTime = datetime.datetime.now()
641 self.historyUpdated.emit()
643 def _update_dnd(self):
646 assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state
648 self._backend[0].is_dnd,
653 _moduleLogger.exception("Reporting error to user")
654 self.error.emit(str(e))
656 if oldDnd != self._dnd:
657 self.dndStateChange(self._dnd)
659 def _perform_op_while_loggedin(self, op):
660 if self.state == self.LOGGEDIN_STATE:
663 self._push_login_op(op)
665 def _push_login_op(self, asyncOp):
666 assert self.state != self.LOGGEDIN_STATE, "Can only queue work when logged out"
667 if asyncOp in self._loginOps:
668 _moduleLogger.info("Skipping queueing duplicate op: %r" % asyncOp)
670 self._loginOps.append(asyncOp)
672 def _alert_on_messages(self, messages):
673 cleanNewMessages = list(self._clean_messages(messages))
674 if self._cleanMessages:
675 if self._cleanMessages != cleanNewMessages:
676 self.newMessages.emit()
677 self._cleanMessages = cleanNewMessages
679 def _clean_messages(self, messages):
680 for message in messages:
683 for kv in message.iteritems()
695 # Don't let outbound messages cause alerts, especially if the package has only outbound
696 cleaned["messageParts"] = [
697 tuple(part[0:-1]) for part in cleaned["messageParts"] if part[0] != "Me:"
699 if not cleaned["messageParts"]: