1 from __future__ import with_statement
14 from PyQt4 import QtCore
16 from util import qore_utils
17 from util import concurrent
18 from util import misc as misc_utils
23 _moduleLogger = logging.getLogger(__name__)
26 class _DraftContact(object):
28 def __init__(self, title, description, numbersWithDescriptions):
30 self.description = description
31 self.numbers = numbersWithDescriptions
32 self.selectedNumber = numbersWithDescriptions[0][0]
35 class Draft(QtCore.QObject):
37 sendingMessage = QtCore.pyqtSignal()
38 sentMessage = QtCore.pyqtSignal()
39 calling = QtCore.pyqtSignal()
40 called = QtCore.pyqtSignal()
41 cancelling = QtCore.pyqtSignal()
42 cancelled = QtCore.pyqtSignal()
43 error = QtCore.pyqtSignal(str)
45 recipientsChanged = QtCore.pyqtSignal()
47 def __init__(self, pool, backend):
48 QtCore.QObject.__init__(self)
51 self._backend = backend
54 assert 0 < len(self._contacts)
55 numbers = [contact.selectedNumber for contact in self._contacts.itervalues()]
56 le = concurrent.AsyncLinearExecution(self._pool, self._send)
57 le.start(numbers, text)
60 assert len(self._contacts) == 1
61 (contact, ) = self._contacts.itervalues()
62 le = concurrent.AsyncLinearExecution(self._pool, self._call)
63 le.start(contact.selectedNumber)
66 le = concurrent.AsyncLinearExecution(self._pool, self._cancel)
69 def add_contact(self, contactId, title, description, numbersWithDescriptions):
70 if contactId in self._contacts:
71 _moduleLogger.info("Adding duplicate contact %r" % contactId)
72 # @todo Remove this evil hack to re-popup the dialog
73 self.recipientsChanged.emit()
75 contactDetails = _DraftContact(title, description, numbersWithDescriptions)
76 self._contacts[contactId] = contactDetails
77 self.recipientsChanged.emit()
79 def remove_contact(self, contactId):
80 assert contactId in self._contacts
81 del self._contacts[contactId]
82 self.recipientsChanged.emit()
84 def get_contacts(self):
85 return self._contacts.iterkeys()
87 def get_num_contacts(self):
88 return len(self._contacts)
90 def get_title(self, cid):
91 return self._contacts[cid].title
93 def get_description(self, cid):
94 return self._contacts[cid].description
96 def get_numbers(self, cid):
97 return self._contacts[cid].numbers
99 def get_selected_number(self, cid):
100 return self._contacts[cid].selectedNumber
102 def set_selected_number(self, cid, number):
103 # @note I'm lazy, this isn't firing any kind of signal since only one
104 # controller right now and that is the viewer
105 assert number in (nWD[0] for nWD in self._contacts[cid].numbers)
106 self._contacts[cid].selectedNumber = number
109 oldContacts = self._contacts
112 self.recipientsChanged.emit()
114 def _send(self, numbers, text):
115 self.sendingMessage.emit()
118 self._backend[0].send_sms,
122 self.sentMessage.emit()
125 self.error.emit(str(e))
127 def _call(self, number):
131 self._backend[0].call,
138 self.error.emit(str(e))
141 self.cancelling.emit()
144 self._backend[0].cancel,
148 self.cancelled.emit()
150 self.error.emit(str(e))
153 class Session(QtCore.QObject):
155 stateChange = QtCore.pyqtSignal(str)
156 loggedOut = QtCore.pyqtSignal()
157 loggedIn = QtCore.pyqtSignal()
158 callbackNumberChanged = QtCore.pyqtSignal(str)
160 contactsUpdated = QtCore.pyqtSignal()
161 messagesUpdated = QtCore.pyqtSignal()
162 historyUpdated = QtCore.pyqtSignal()
163 dndStateChange = QtCore.pyqtSignal(bool)
165 error = QtCore.pyqtSignal(str)
167 LOGGEDOUT_STATE = "logged out"
168 LOGGINGIN_STATE = "logging in"
169 LOGGEDIN_STATE = "logged in"
171 _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.2.0")
176 def __init__(self, cachePath = None):
177 QtCore.QObject.__init__(self)
178 self._pool = qore_utils.AsyncPool()
180 self._loggedInTime = self._LOGGEDOUT_TIME
182 self._cachePath = cachePath
183 self._username = None
184 self._draft = Draft(self._pool, self._backend)
187 self._contactUpdateTime = datetime.datetime(1971, 1, 1)
189 self._messageUpdateTime = datetime.datetime(1971, 1, 1)
191 self._historyUpdateTime = datetime.datetime(1971, 1, 1)
198 self._LOGGEDOUT_TIME: self.LOGGEDOUT_STATE,
199 self._LOGGINGIN_TIME: self.LOGGINGIN_STATE,
200 }.get(self._loggedInTime, self.LOGGEDIN_STATE)
206 def login(self, username, password):
207 assert self.state == self.LOGGEDOUT_STATE
208 assert username != ""
209 if self._cachePath is not None:
210 cookiePath = os.path.join(self._cachePath, "%s.cookies" % username)
214 if self._username != username or not self._backend:
215 from backends import gv_backend
217 self._backend[0:0] = [gv_backend.GVDialer(cookiePath)]
220 le = concurrent.AsyncLinearExecution(self._pool, self._login)
221 le.start(username, password)
224 assert self.state != self.LOGGEDOUT_STATE
226 self._loggedInTime = self._LOGGEDOUT_TIME
227 self._backend[0].persist()
228 self._save_to_cache()
231 assert self.state == self.LOGGEDOUT_STATE
232 self._backend[0].logout()
237 def logout_and_clear(self):
238 assert self.state != self.LOGGEDOUT_STATE
240 self._loggedInTime = self._LOGGEDOUT_TIME
243 def update_contacts(self, force = True):
244 if not force and self._contacts:
246 le = concurrent.AsyncLinearExecution(self._pool, self._update_contacts)
247 self._perform_op_while_loggedin(le)
249 def get_contacts(self):
250 return self._contacts
252 def get_when_contacts_updated(self):
253 return self._contactUpdateTime
255 def update_messages(self, force = True):
256 if not force and self._messages:
258 le = concurrent.AsyncLinearExecution(self._pool, self._update_messages)
259 self._perform_op_while_loggedin(le)
261 def get_messages(self):
262 return self._messages
264 def get_when_messages_updated(self):
265 return self._messageUpdateTime
267 def update_history(self, force = True):
268 if not force and self._history:
270 le = concurrent.AsyncLinearExecution(self._pool, self._update_history)
271 self._perform_op_while_loggedin(le)
273 def get_history(self):
276 def get_when_history_updated(self):
277 return self._historyUpdateTime
279 def update_dnd(self):
280 le = concurrent.AsyncLinearExecution(self._pool, self._update_dnd)
281 self._perform_op_while_loggedin(le)
283 def set_dnd(self, dnd):
284 le = concurrent.AsyncLinearExecution(self._pool, self._set_dnd)
287 def _set_dnd(self, dnd):
288 # I'm paranoid about our state geting out of sync so we set no matter
289 # what but act as if we have the cannonical state
290 assert self.state == self.LOGGEDIN_STATE
294 self._backend[0].set_dnd,
299 self.error.emit(str(e))
302 if oldDnd != self._dnd:
303 self.dndStateChange.emit(self._dnd)
308 def get_account_number(self):
309 return self._backend[0].get_account_number()
311 def get_callback_numbers(self):
312 # @todo Remove evilness (might call is_authed which can block)
313 return self._backend[0].get_callback_numbers()
315 def get_callback_number(self):
316 return self._callback
318 def set_callback_number(self, callback):
319 le = concurrent.AsyncLinearExecution(self._pool, self._set_callback_number)
322 def _set_callback_number(self, callback):
323 # I'm paranoid about our state geting out of sync so we set no matter
324 # what but act as if we have the cannonical state
325 assert self.state == self.LOGGEDIN_STATE
326 oldCallback = self._callback
329 self._backend[0].set_callback_number,
334 self.error.emit(str(e))
336 self._callback = callback
337 if oldCallback != self._callback:
338 self.callbackNumberChanged.emit(self._callback)
340 def _login(self, username, password):
341 self._loggedInTime = self._LOGGINGIN_TIME
342 self.stateChange.emit(self.LOGGINGIN_STATE)
343 finalState = self.LOGGEDOUT_STATE
347 if not isLoggedIn and self._backend[0].is_quick_login_possible():
349 self._backend[0].is_authed,
354 _moduleLogger.info("Logged in through cookies")
356 # Force a clearing of the cookies
358 self._backend[0].logout,
365 self._backend[0].login,
366 (username, password),
370 _moduleLogger.info("Logged in through credentials")
373 self._loggedInTime = int(time.time())
374 oldUsername = self._username
375 self._username = username
376 finalState = self.LOGGEDIN_STATE
378 if oldUsername != self._username:
379 needOps = not self._load()
383 loginOps = self._loginOps[:]
386 del self._loginOps[:]
387 for asyncOp in loginOps:
390 self.error.emit(str(e))
392 self.stateChange.emit(finalState)
395 updateContacts = len(self._contacts) != 0
396 updateMessages = len(self._messages) != 0
397 updateHistory = len(self._history) != 0
399 oldCallback = self._callback
407 loadedFromCache = self._load_from_cache()
409 updateContacts = True
410 updateMessages = True
414 self.contactsUpdated.emit()
416 self.messagesUpdated.emit()
418 self.historyUpdated.emit()
419 if oldDnd != self._dnd:
420 self.dndStateChange.emit(self._dnd)
421 if oldCallback != self._callback:
422 self.callbackNumberChanged.emit(self._callback)
424 return loadedFromCache
426 def _load_from_cache(self):
427 if self._cachePath is None:
429 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
432 with open(cachePath, "rb") as f:
433 dumpedData = pickle.load(f)
434 except (pickle.PickleError, IOError, EOFError, ValueError):
435 _moduleLogger.exception("Pickle fun loading")
438 _moduleLogger.exception("Weirdness loading")
443 contacts, contactUpdateTime,
444 messages, messageUpdateTime,
445 history, historyUpdateTime,
449 if misc_utils.compare_versions(
450 self._OLDEST_COMPATIBLE_FORMAT_VERSION,
451 misc_utils.parse_version(version),
453 _moduleLogger.info("Loaded cache")
454 self._contacts = contacts
455 self._contactUpdateTime = contactUpdateTime
456 self._messages = messages
457 self._messageUpdateTime = messageUpdateTime
458 self._history = history
459 self._historyUpdateTime = historyUpdateTime
461 self._callback = callback
465 "Skipping cache due to version mismatch (%s-%s)" % (
471 def _save_to_cache(self):
472 _moduleLogger.info("Saving cache")
473 if self._cachePath is None:
475 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
479 constants.__version__, constants.__build__,
480 self._contacts, self._contactUpdateTime,
481 self._messages, self._messageUpdateTime,
482 self._history, self._historyUpdateTime,
483 self._dnd, self._callback
485 with open(cachePath, "wb") as f:
486 pickle.dump(dataToDump, f, pickle.HIGHEST_PROTOCOL)
487 _moduleLogger.info("Cache saved")
488 except (pickle.PickleError, IOError):
489 _moduleLogger.exception("While saving")
491 def _clear_cache(self):
492 updateContacts = len(self._contacts) != 0
493 updateMessages = len(self._messages) != 0
494 updateHistory = len(self._history) != 0
496 oldCallback = self._callback
499 self._contactUpdateTime = datetime.datetime(1, 1, 1)
501 self._messageUpdateTime = datetime.datetime(1, 1, 1)
503 self._historyUpdateTime = datetime.datetime(1, 1, 1)
508 self.contactsUpdated.emit()
510 self.messagesUpdated.emit()
512 self.historyUpdated.emit()
513 if oldDnd != self._dnd:
514 self.dndStateChange.emit(self._dnd)
515 if oldCallback != self._callback:
516 self.callbackNumberChanged.emit(self._callback)
518 self._save_to_cache()
520 def _update_contacts(self):
522 self._contacts = yield (
523 self._backend[0].get_contacts,
528 self.error.emit(str(e))
530 self._contactUpdateTime = datetime.datetime.now()
531 self.contactsUpdated.emit()
533 def _update_messages(self):
535 self._messages = yield (
536 self._backend[0].get_messages,
541 self.error.emit(str(e))
543 self._messageUpdateTime = datetime.datetime.now()
544 self.messagesUpdated.emit()
546 def _update_history(self):
548 self._history = yield (
549 self._backend[0].get_recent,
554 self.error.emit(str(e))
556 self._historyUpdateTime = datetime.datetime.now()
557 self.historyUpdated.emit()
559 def _update_dnd(self):
563 self._backend[0].is_dnd,
568 self.error.emit(str(e))
570 if oldDnd != self._dnd:
571 self.dndStateChange(self._dnd)
573 def _perform_op_while_loggedin(self, op):
574 if self.state == self.LOGGEDIN_STATE:
577 self._push_login_op(op)
579 def _push_login_op(self, asyncOp):
580 assert self.state != self.LOGGEDIN_STATE
581 if asyncOp in self._loginOps:
582 _moduleLogger.info("Skipping queueing duplicate op: %r" % asyncOp)
584 self._loginOps.append(asyncOp)