1 from __future__ import with_statement
13 from PyQt4 import QtCore
15 from util import qore_utils
16 from util import concurrent
17 from util import misc as misc_utils
22 _moduleLogger = logging.getLogger(__name__)
25 class _DraftContact(object):
27 def __init__(self, title, description, numbersWithDescriptions):
29 self.description = description
30 self.numbers = numbersWithDescriptions
31 self.selectedNumber = numbersWithDescriptions[0][0]
34 class Draft(QtCore.QObject):
36 sendingMessage = QtCore.pyqtSignal()
37 sentMessage = QtCore.pyqtSignal()
38 calling = QtCore.pyqtSignal()
39 called = QtCore.pyqtSignal()
40 cancelling = QtCore.pyqtSignal()
41 cancelled = QtCore.pyqtSignal()
42 error = QtCore.pyqtSignal(str)
44 recipientsChanged = QtCore.pyqtSignal()
46 def __init__(self, pool, backend):
47 QtCore.QObject.__init__(self)
50 self._backend = backend
53 assert 0 < len(self._contacts)
54 numbers = [contact.selectedNumber for contact in self._contacts.itervalues()]
55 le = concurrent.AsyncLinearExecution(self._pool, self._send)
56 le.start(numbers, text)
59 assert len(self._contacts) == 1
60 (contact, ) = self._contacts.itervalues()
61 le = concurrent.AsyncLinearExecution(self._pool, self._call)
62 le.start(contact.selectedNumber)
65 le = concurrent.AsyncLinearExecution(self._pool, self._cancel)
68 def add_contact(self, contactId, title, description, numbersWithDescriptions):
69 assert contactId not in self._contacts
70 contactDetails = _DraftContact(title, description, numbersWithDescriptions)
71 self._contacts[contactId] = contactDetails
72 self.recipientsChanged.emit()
74 def remove_contact(self, contactId):
75 assert contactId in self._contacts
76 del self._contacts[contactId]
77 self.recipientsChanged.emit()
79 def get_contacts(self):
80 return self._contacts.iterkeys()
82 def get_num_contacts(self):
83 return len(self._contacts)
85 def get_title(self, cid):
86 return self._contacts[cid].title
88 def get_description(self, cid):
89 return self._contacts[cid].description
91 def get_numbers(self, cid):
92 return self._contacts[cid].numbers
94 def get_selected_number(self, cid):
95 return self._contacts[cid].selectedNumber
97 def set_selected_number(self, cid, number):
98 # @note I'm lazy, this isn't firing any kind of signal since only one
99 # controller right now and that is the viewer
100 assert number in (nWD[0] for nWD in self._contacts[cid].numbers)
101 self._contacts[cid].selectedNumber = number
104 oldContacts = self._contacts
107 self.recipientsChanged.emit()
109 def _send(self, numbers, text):
110 self.sendingMessage.emit()
113 self._backend[0].send_sms,
117 self.sentMessage.emit()
120 self.error.emit(str(e))
122 def _call(self, number):
126 self._backend[0].call,
133 self.error.emit(str(e))
136 self.cancelling.emit()
139 self._backend[0].cancel,
143 self.cancelled.emit()
145 self.error.emit(str(e))
148 class Session(QtCore.QObject):
150 stateChange = QtCore.pyqtSignal(str)
151 loggedOut = QtCore.pyqtSignal()
152 loggedIn = QtCore.pyqtSignal()
153 callbackNumberChanged = QtCore.pyqtSignal(str)
155 contactsUpdated = QtCore.pyqtSignal()
156 messagesUpdated = QtCore.pyqtSignal()
157 historyUpdated = QtCore.pyqtSignal()
158 dndStateChange = QtCore.pyqtSignal(bool)
160 error = QtCore.pyqtSignal(str)
162 LOGGEDOUT_STATE = "logged out"
163 LOGGINGIN_STATE = "logging in"
164 LOGGEDIN_STATE = "logged in"
166 _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.2.0")
171 def __init__(self, cachePath = None):
172 QtCore.QObject.__init__(self)
173 self._pool = qore_utils.AsyncPool()
175 self._loggedInTime = self._LOGGEDOUT_TIME
177 self._cachePath = cachePath
178 self._username = None
179 self._draft = Draft(self._pool, self._backend)
190 self._LOGGEDOUT_TIME: self.LOGGEDOUT_STATE,
191 self._LOGGINGIN_TIME: self.LOGGINGIN_STATE,
192 }.get(self._loggedInTime, self.LOGGEDIN_STATE)
198 def login(self, username, password):
199 assert self.state == self.LOGGEDOUT_STATE
200 assert username != ""
201 if self._cachePath is not None:
202 cookiePath = os.path.join(self._cachePath, "%s.cookies" % username)
206 if self._username != username or not self._backend:
207 from backends import gv_backend
209 self._backend[0:0] = [gv_backend.GVDialer(cookiePath)]
212 le = concurrent.AsyncLinearExecution(self._pool, self._login)
213 le.start(username, password)
216 assert self.state != self.LOGGEDOUT_STATE
218 self._loggedInTime = self._LOGGEDOUT_TIME
219 self._backend[0].persist()
220 self._save_to_cache()
223 assert self.state == self.LOGGEDOUT_STATE
224 self._backend[0].logout()
229 def logout_and_clear(self):
230 assert self.state != self.LOGGEDOUT_STATE
232 self._loggedInTime = self._LOGGEDOUT_TIME
235 def update_contacts(self, force = True):
236 if not force and self._contacts:
238 le = concurrent.AsyncLinearExecution(self._pool, self._update_contacts)
239 self._perform_op_while_loggedin(le)
241 def get_contacts(self):
242 return self._contacts
244 def update_messages(self, force = True):
245 if not force and self._messages:
247 le = concurrent.AsyncLinearExecution(self._pool, self._update_messages)
248 self._perform_op_while_loggedin(le)
250 def get_messages(self):
251 return self._messages
253 def update_history(self, force = True):
254 if not force and self._history:
256 le = concurrent.AsyncLinearExecution(self._pool, self._update_history)
257 self._perform_op_while_loggedin(le)
259 def get_history(self):
262 def update_dnd(self):
263 le = concurrent.AsyncLinearExecution(self._pool, self._update_dnd)
264 self._perform_op_while_loggedin(le)
266 def set_dnd(self, dnd):
267 # I'm paranoid about our state geting out of sync so we set no matter
268 # what but act as if we have the cannonical state
269 assert self.state == self.LOGGEDIN_STATE
273 self._backend[0].set_dnd,
278 self.error.emit(str(e))
281 if oldDnd != self._dnd:
282 self.dndStateChange.emit(self._dnd)
287 def get_account_number(self):
288 return self._backend[0].get_account_number()
290 def get_callback_numbers(self):
291 # @todo Remove evilness
292 return self._backend[0].get_callback_numbers()
294 def get_callback_number(self):
295 return self._callback
297 def set_callback_number(self, callback):
298 # I'm paranoid about our state geting out of sync so we set no matter
299 # what but act as if we have the cannonical state
300 assert self.state == self.LOGGEDIN_STATE
301 oldCallback = self._callback
304 self._backend[0].set_callback_number,
309 self.error.emit(str(e))
311 self._callback = callback
312 if oldCallback != self._callback:
313 self.callbackNumberChanged.emit(self._callback)
315 def _login(self, username, password):
316 self._loggedInTime = self._LOGGINGIN_TIME
317 self.stateChange.emit(self.LOGGINGIN_STATE)
318 finalState = self.LOGGEDOUT_STATE
322 if not isLoggedIn and self._backend[0].is_quick_login_possible():
324 self._backend[0].is_authed,
329 _moduleLogger.info("Logged in through cookies")
331 # Force a clearing of the cookies
333 self._backend[0].logout,
340 self._backend[0].login,
341 (username, password),
345 _moduleLogger.info("Logged in through credentials")
348 self._loggedInTime = int(time.time())
349 oldUsername = self._username
350 self._username = username
351 finalState = self.LOGGEDIN_STATE
353 if oldUsername != self._username:
354 needOps = not self._load()
358 loginOps = self._loginOps[:]
361 del self._loginOps[:]
362 for asyncOp in loginOps:
365 self.error.emit(str(e))
367 self.stateChange.emit(finalState)
370 updateContacts = len(self._contacts) != 0
371 updateMessages = len(self._messages) != 0
372 updateHistory = len(self._history) != 0
374 oldCallback = self._callback
382 loadedFromCache = self._load_from_cache()
384 updateContacts = True
385 updateMessages = True
389 self.contactsUpdated.emit()
391 self.messagesUpdated.emit()
393 self.historyUpdated.emit()
394 if oldDnd != self._dnd:
395 self.dndStateChange.emit(self._dnd)
396 if oldCallback != self._callback:
397 self.callbackNumberChanged.emit(self._callback)
399 return loadedFromCache
401 def _load_from_cache(self):
402 if self._cachePath is None:
404 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
407 with open(cachePath, "rb") as f:
408 dumpedData = pickle.load(f)
409 except (pickle.PickleError, IOError, EOFError, ValueError):
410 _moduleLogger.exception("Pickle fun loading")
413 _moduleLogger.exception("Weirdness loading")
418 contacts, messages, history, dnd, callback
421 if misc_utils.compare_versions(
422 self._OLDEST_COMPATIBLE_FORMAT_VERSION,
423 misc_utils.parse_version(version),
425 _moduleLogger.info("Loaded cache")
426 self._contacts = contacts
427 self._messages = messages
428 self._history = history
430 self._callback = callback
434 "Skipping cache due to version mismatch (%s-%s)" % (
440 def _save_to_cache(self):
441 _moduleLogger.info("Saving cache")
442 if self._cachePath is None:
444 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
448 constants.__version__, constants.__build__,
449 self._contacts, self._messages, self._history, self._dnd, self._callback
451 with open(cachePath, "wb") as f:
452 pickle.dump(dataToDump, f, pickle.HIGHEST_PROTOCOL)
453 _moduleLogger.info("Cache saved")
454 except (pickle.PickleError, IOError):
455 _moduleLogger.exception("While saving")
457 def _clear_cache(self):
458 updateContacts = len(self._contacts) != 0
459 updateMessages = len(self._messages) != 0
460 updateHistory = len(self._history) != 0
462 oldCallback = self._callback
471 self.contactsUpdated.emit()
473 self.messagesUpdated.emit()
475 self.historyUpdated.emit()
476 if oldDnd != self._dnd:
477 self.dndStateChange.emit(self._dnd)
478 if oldCallback != self._callback:
479 self.callbackNumberChanged.emit(self._callback)
481 self._save_to_cache()
483 def _update_contacts(self):
485 self._contacts = yield (
486 self._backend[0].get_contacts,
491 self.error.emit(str(e))
493 self.contactsUpdated.emit()
495 def _update_messages(self):
497 self._messages = yield (
498 self._backend[0].get_messages,
503 self.error.emit(str(e))
505 self.messagesUpdated.emit()
507 def _update_history(self):
509 self._history = yield (
510 self._backend[0].get_recent,
515 self.error.emit(str(e))
517 self.historyUpdated.emit()
519 def _update_dnd(self):
523 self._backend[0].is_dnd,
528 self.error.emit(str(e))
530 if oldDnd != self._dnd:
531 self.dndStateChange(self._dnd)
533 def _perform_op_while_loggedin(self, op):
534 if self.state == self.LOGGEDIN_STATE:
537 self._push_login_op(op)
539 def _push_login_op(self, asyncOp):
540 assert self.state != self.LOGGEDIN_STATE
541 if asyncOp in self._loginOps:
542 _moduleLogger.info("Skipping queueing duplicate op: %r" % asyncOp)
544 self._loginOps.append(asyncOp)