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
58 assert 0 < len(self._contacts), "No contacts selected"
59 numbers = [misc_utils.make_ugly(contact.selectedNumber) for contact in self._contacts.itervalues()]
60 le = concurrent.AsyncLinearExecution(self._pool, self._send)
61 le.start(numbers, text)
64 assert len(self._contacts) == 1, "Must select 1 and only 1 contact"
65 (contact, ) = self._contacts.itervalues()
66 number = misc_utils.make_ugly(contact.selectedNumber)
67 le = concurrent.AsyncLinearExecution(self._pool, self._call)
71 le = concurrent.AsyncLinearExecution(self._pool, self._cancel)
74 def add_contact(self, contactId, title, description, numbersWithDescriptions):
75 if self._busyReason is not None:
76 raise RuntimeError("Please wait for %r" % self._busyReason)
77 if contactId in self._contacts:
78 _moduleLogger.info("Adding duplicate contact %r" % contactId)
79 # @todo Remove this evil hack to re-popup the dialog
80 self.recipientsChanged.emit()
82 contactDetails = _DraftContact(title, description, numbersWithDescriptions)
83 self._contacts[contactId] = contactDetails
84 self.recipientsChanged.emit()
86 def remove_contact(self, contactId):
87 if self._busyReason is not None:
88 raise RuntimeError("Please wait for %r" % self._busyReason)
89 assert contactId in self._contacts, "Contact missing"
90 del self._contacts[contactId]
91 self.recipientsChanged.emit()
93 def get_contacts(self):
94 return self._contacts.iterkeys()
96 def get_num_contacts(self):
97 return len(self._contacts)
99 def get_title(self, cid):
100 return self._contacts[cid].title
102 def get_description(self, cid):
103 return self._contacts[cid].description
105 def get_numbers(self, cid):
106 return self._contacts[cid].numbers
108 def get_selected_number(self, cid):
109 return self._contacts[cid].selectedNumber
111 def set_selected_number(self, cid, number):
112 # @note I'm lazy, this isn't firing any kind of signal since only one
113 # controller right now and that is the viewer
114 assert number in (nWD[0] for nWD in self._contacts[cid].numbers), "Number not selectable"
115 self._contacts[cid].selectedNumber = number
118 if self._busyReason is not None:
119 raise RuntimeError("Please wait for %r" % self._busyReason)
123 oldContacts = self._contacts
126 self.recipientsChanged.emit()
128 @contextlib.contextmanager
129 def _busy(self, message):
130 if self._busyReason is not None:
131 raise RuntimeError("Already busy doing %r" % self._busyReason)
133 self._busyReason = message
136 self._busyReason = None
138 def _send(self, numbers, text):
139 self.sendingMessage.emit()
141 with self._busy("Sending Text"):
142 with qui_utils.notify_busy(self._errorLog, "Sending Text"):
144 self._backend[0].send_sms,
148 self.sentMessage.emit()
151 self.error.emit(str(e))
153 def _call(self, number):
156 with self._busy("Calling"):
157 with qui_utils.notify_busy(self._errorLog, "Calling"):
159 self._backend[0].call,
166 self.error.emit(str(e))
169 self.cancelling.emit()
171 with qui_utils.notify_busy(self._errorLog, "Cancelling"):
173 self._backend[0].cancel,
177 self.cancelled.emit()
179 self.error.emit(str(e))
182 class Session(QtCore.QObject):
184 # @todo Somehow add support for csv contacts
186 stateChange = QtCore.pyqtSignal(str)
187 loggedOut = QtCore.pyqtSignal()
188 loggedIn = QtCore.pyqtSignal()
189 callbackNumberChanged = QtCore.pyqtSignal(str)
191 contactsUpdated = QtCore.pyqtSignal()
192 messagesUpdated = QtCore.pyqtSignal()
193 historyUpdated = QtCore.pyqtSignal()
194 dndStateChange = QtCore.pyqtSignal(bool)
196 error = QtCore.pyqtSignal(str)
198 LOGGEDOUT_STATE = "logged out"
199 LOGGINGIN_STATE = "logging in"
200 LOGGEDIN_STATE = "logged in"
202 _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.1.90")
207 def __init__(self, errorLog, cachePath = None):
208 QtCore.QObject.__init__(self)
209 self._errorLog = errorLog
210 self._pool = qore_utils.AsyncPool()
212 self._loggedInTime = self._LOGGEDOUT_TIME
214 self._cachePath = cachePath
215 self._username = None
216 self._draft = Draft(self._pool, self._backend, self._errorLog)
219 self._contactUpdateTime = datetime.datetime(1971, 1, 1)
221 self._messageUpdateTime = datetime.datetime(1971, 1, 1)
223 self._historyUpdateTime = datetime.datetime(1971, 1, 1)
230 self._LOGGEDOUT_TIME: self.LOGGEDOUT_STATE,
231 self._LOGGINGIN_TIME: self.LOGGINGIN_STATE,
232 }.get(self._loggedInTime, self.LOGGEDIN_STATE)
238 def login(self, username, password):
239 assert self.state == self.LOGGEDOUT_STATE, "Can only log-in when logged out (currently %s" % self.state
240 assert username != "", "No username specified"
241 if self._cachePath is not None:
242 cookiePath = os.path.join(self._cachePath, "%s.cookies" % username)
246 if self._username != username or not self._backend:
247 from backends import gv_backend
249 self._backend[0:0] = [gv_backend.GVDialer(cookiePath)]
252 le = concurrent.AsyncLinearExecution(self._pool, self._login)
253 le.start(username, password)
256 assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state
258 self._loggedInTime = self._LOGGEDOUT_TIME
259 self._backend[0].persist()
260 self._save_to_cache()
263 assert self.state == self.LOGGEDOUT_STATE, "Can only clear when logged out (currently %s" % self.state
264 self._backend[0].logout()
269 def logout_and_clear(self):
270 assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state
272 self._loggedInTime = self._LOGGEDOUT_TIME
275 def update_contacts(self, force = True):
276 if not force and self._contacts:
278 le = concurrent.AsyncLinearExecution(self._pool, self._update_contacts)
279 self._perform_op_while_loggedin(le)
281 def get_contacts(self):
282 return self._contacts
284 def get_when_contacts_updated(self):
285 return self._contactUpdateTime
287 def update_messages(self, force = True):
288 if not force and self._messages:
290 le = concurrent.AsyncLinearExecution(self._pool, self._update_messages)
291 self._perform_op_while_loggedin(le)
293 def get_messages(self):
294 return self._messages
296 def get_when_messages_updated(self):
297 return self._messageUpdateTime
299 def update_history(self, force = True):
300 if not force and self._history:
302 le = concurrent.AsyncLinearExecution(self._pool, self._update_history)
303 self._perform_op_while_loggedin(le)
305 def get_history(self):
308 def get_when_history_updated(self):
309 return self._historyUpdateTime
311 def update_dnd(self):
312 le = concurrent.AsyncLinearExecution(self._pool, self._update_dnd)
313 self._perform_op_while_loggedin(le)
315 def set_dnd(self, dnd):
316 le = concurrent.AsyncLinearExecution(self._pool, self._set_dnd)
319 def _set_dnd(self, dnd):
320 # I'm paranoid about our state geting out of sync so we set no matter
321 # what but act as if we have the cannonical state
322 assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state
325 with qui_utils.notify_busy(self._errorLog, "Setting DND Status"):
327 self._backend[0].set_dnd,
332 self.error.emit(str(e))
335 if oldDnd != self._dnd:
336 self.dndStateChange.emit(self._dnd)
341 def get_account_number(self):
342 return self._backend[0].get_account_number()
344 def get_callback_numbers(self):
345 # @todo Remove evilness (might call is_authed which can block)
346 return self._backend[0].get_callback_numbers()
348 def get_callback_number(self):
349 return self._callback
351 def set_callback_number(self, callback):
352 le = concurrent.AsyncLinearExecution(self._pool, self._set_callback_number)
355 def _set_callback_number(self, callback):
356 # I'm paranoid about our state geting out of sync so we set no matter
357 # what but act as if we have the cannonical state
358 assert self.state == self.LOGGEDIN_STATE, "Callbacks configurable only when logged in (currently %s" % self.state
359 oldCallback = self._callback
362 self._backend[0].set_callback_number,
367 self.error.emit(str(e))
369 self._callback = callback
370 if oldCallback != self._callback:
371 self.callbackNumberChanged.emit(self._callback)
373 def _login(self, username, password):
374 with qui_utils.notify_busy(self._errorLog, "Logging In"):
375 self._loggedInTime = self._LOGGINGIN_TIME
376 self.stateChange.emit(self.LOGGINGIN_STATE)
377 finalState = self.LOGGEDOUT_STATE
380 if not isLoggedIn and self._backend[0].is_quick_login_possible():
382 self._backend[0].is_authed,
387 _moduleLogger.info("Logged in through cookies")
389 # Force a clearing of the cookies
391 self._backend[0].logout,
398 self._backend[0].login,
399 (username, password),
403 _moduleLogger.info("Logged in through credentials")
406 self._loggedInTime = int(time.time())
407 oldUsername = self._username
408 self._username = username
409 finalState = self.LOGGEDIN_STATE
410 if oldUsername != self._username:
411 needOps = not self._load()
418 loginOps = self._loginOps[:]
421 del self._loginOps[:]
422 for asyncOp in loginOps:
425 self._loggedInTime = self._LOGGEDOUT_TIME
426 self.error.emit("Error logging in")
428 self._loggedInTime = self._LOGGEDOUT_TIME
429 self.error.emit(str(e))
431 self.stateChange.emit(finalState)
432 if isLoggedIn and self._callback:
433 self.set_callback_number(self._callback)
436 updateContacts = len(self._contacts) != 0
437 updateMessages = len(self._messages) != 0
438 updateHistory = len(self._history) != 0
440 oldCallback = self._callback
448 loadedFromCache = self._load_from_cache()
450 updateContacts = True
451 updateMessages = True
455 self.contactsUpdated.emit()
457 self.messagesUpdated.emit()
459 self.historyUpdated.emit()
460 if oldDnd != self._dnd:
461 self.dndStateChange.emit(self._dnd)
462 if oldCallback != self._callback:
463 self.callbackNumberChanged.emit(self._callback)
465 return loadedFromCache
467 def _load_from_cache(self):
468 if self._cachePath is None:
470 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
473 with open(cachePath, "rb") as f:
474 dumpedData = pickle.load(f)
475 except (pickle.PickleError, IOError, EOFError, ValueError):
476 _moduleLogger.exception("Pickle fun loading")
479 _moduleLogger.exception("Weirdness loading")
484 contacts, contactUpdateTime,
485 messages, messageUpdateTime,
486 history, historyUpdateTime,
490 if misc_utils.compare_versions(
491 self._OLDEST_COMPATIBLE_FORMAT_VERSION,
492 misc_utils.parse_version(version),
494 _moduleLogger.info("Loaded cache")
495 self._contacts = contacts
496 self._contactUpdateTime = contactUpdateTime
497 self._messages = messages
498 self._messageUpdateTime = messageUpdateTime
499 self._history = history
500 self._historyUpdateTime = historyUpdateTime
502 self._callback = callback
506 "Skipping cache due to version mismatch (%s-%s)" % (
512 def _save_to_cache(self):
513 _moduleLogger.info("Saving cache")
514 if self._cachePath is None:
516 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
520 constants.__version__, constants.__build__,
521 self._contacts, self._contactUpdateTime,
522 self._messages, self._messageUpdateTime,
523 self._history, self._historyUpdateTime,
524 self._dnd, self._callback
526 with open(cachePath, "wb") as f:
527 pickle.dump(dataToDump, f, pickle.HIGHEST_PROTOCOL)
528 _moduleLogger.info("Cache saved")
529 except (pickle.PickleError, IOError):
530 _moduleLogger.exception("While saving")
532 def _clear_cache(self):
533 updateContacts = len(self._contacts) != 0
534 updateMessages = len(self._messages) != 0
535 updateHistory = len(self._history) != 0
537 oldCallback = self._callback
540 self._contactUpdateTime = datetime.datetime(1, 1, 1)
542 self._messageUpdateTime = datetime.datetime(1, 1, 1)
544 self._historyUpdateTime = datetime.datetime(1, 1, 1)
549 self.contactsUpdated.emit()
551 self.messagesUpdated.emit()
553 self.historyUpdated.emit()
554 if oldDnd != self._dnd:
555 self.dndStateChange.emit(self._dnd)
556 if oldCallback != self._callback:
557 self.callbackNumberChanged.emit(self._callback)
559 self._save_to_cache()
561 def _update_contacts(self):
563 with qui_utils.notify_busy(self._errorLog, "Updating Contacts"):
564 self._contacts = yield (
565 self._backend[0].get_contacts,
570 self.error.emit(str(e))
572 self._contactUpdateTime = datetime.datetime.now()
573 self.contactsUpdated.emit()
575 def _update_messages(self):
577 with qui_utils.notify_busy(self._errorLog, "Updating Messages"):
578 self._messages = yield (
579 self._backend[0].get_messages,
584 self.error.emit(str(e))
586 self._messageUpdateTime = datetime.datetime.now()
587 self.messagesUpdated.emit()
589 def _update_history(self):
591 with qui_utils.notify_busy(self._errorLog, "Updating History"):
592 self._history = yield (
593 self._backend[0].get_recent,
598 self.error.emit(str(e))
600 self._historyUpdateTime = datetime.datetime.now()
601 self.historyUpdated.emit()
603 def _update_dnd(self):
607 self._backend[0].is_dnd,
612 self.error.emit(str(e))
614 if oldDnd != self._dnd:
615 self.dndStateChange(self._dnd)
617 def _perform_op_while_loggedin(self, op):
618 if self.state == self.LOGGEDIN_STATE:
621 self._push_login_op(op)
623 def _push_login_op(self, asyncOp):
624 assert self.state != self.LOGGEDIN_STATE, "Can only queue work when logged out"
625 if asyncOp in self._loginOps:
626 _moduleLogger.info("Skipping queueing duplicate op: %r" % asyncOp)
628 self._loginOps.append(asyncOp)