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 # I'm paranoid about our state geting out of sync so we set no matter
285 # what but act as if we have the cannonical state
286 assert self.state == self.LOGGEDIN_STATE
290 self._backend[0].set_dnd,
295 self.error.emit(str(e))
298 if oldDnd != self._dnd:
299 self.dndStateChange.emit(self._dnd)
304 def get_account_number(self):
305 return self._backend[0].get_account_number()
307 def get_callback_numbers(self):
308 # @todo Remove evilness (might call is_authed which can block)
309 return self._backend[0].get_callback_numbers()
311 def get_callback_number(self):
312 return self._callback
314 def set_callback_number(self, callback):
315 # I'm paranoid about our state geting out of sync so we set no matter
316 # what but act as if we have the cannonical state
317 assert self.state == self.LOGGEDIN_STATE
318 oldCallback = self._callback
321 self._backend[0].set_callback_number,
326 self.error.emit(str(e))
328 self._callback = callback
329 if oldCallback != self._callback:
330 self.callbackNumberChanged.emit(self._callback)
332 def _login(self, username, password):
333 self._loggedInTime = self._LOGGINGIN_TIME
334 self.stateChange.emit(self.LOGGINGIN_STATE)
335 finalState = self.LOGGEDOUT_STATE
339 if not isLoggedIn and self._backend[0].is_quick_login_possible():
341 self._backend[0].is_authed,
346 _moduleLogger.info("Logged in through cookies")
348 # Force a clearing of the cookies
350 self._backend[0].logout,
357 self._backend[0].login,
358 (username, password),
362 _moduleLogger.info("Logged in through credentials")
365 self._loggedInTime = int(time.time())
366 oldUsername = self._username
367 self._username = username
368 finalState = self.LOGGEDIN_STATE
370 if oldUsername != self._username:
371 needOps = not self._load()
375 loginOps = self._loginOps[:]
378 del self._loginOps[:]
379 for asyncOp in loginOps:
382 self.error.emit(str(e))
384 self.stateChange.emit(finalState)
387 updateContacts = len(self._contacts) != 0
388 updateMessages = len(self._messages) != 0
389 updateHistory = len(self._history) != 0
391 oldCallback = self._callback
399 loadedFromCache = self._load_from_cache()
401 updateContacts = True
402 updateMessages = True
406 self.contactsUpdated.emit()
408 self.messagesUpdated.emit()
410 self.historyUpdated.emit()
411 if oldDnd != self._dnd:
412 self.dndStateChange.emit(self._dnd)
413 if oldCallback != self._callback:
414 self.callbackNumberChanged.emit(self._callback)
416 return loadedFromCache
418 def _load_from_cache(self):
419 if self._cachePath is None:
421 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
424 with open(cachePath, "rb") as f:
425 dumpedData = pickle.load(f)
426 except (pickle.PickleError, IOError, EOFError, ValueError):
427 _moduleLogger.exception("Pickle fun loading")
430 _moduleLogger.exception("Weirdness loading")
435 contacts, contactUpdateTime,
436 messages, messageUpdateTime,
437 history, historyUpdateTime,
441 if misc_utils.compare_versions(
442 self._OLDEST_COMPATIBLE_FORMAT_VERSION,
443 misc_utils.parse_version(version),
445 _moduleLogger.info("Loaded cache")
446 self._contacts = contacts
447 self._contactUpdateTime = contactUpdateTime
448 self._messages = messages
449 self._messageUpdateTime = messageUpdateTime
450 self._history = history
451 self._historyUpdateTime = historyUpdateTime
453 self._callback = callback
457 "Skipping cache due to version mismatch (%s-%s)" % (
463 def _save_to_cache(self):
464 _moduleLogger.info("Saving cache")
465 if self._cachePath is None:
467 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
471 constants.__version__, constants.__build__,
472 self._contacts, self._contactUpdateTime,
473 self._messages, self._messageUpdateTime,
474 self._history, self._historyUpdateTime,
475 self._dnd, self._callback
477 with open(cachePath, "wb") as f:
478 pickle.dump(dataToDump, f, pickle.HIGHEST_PROTOCOL)
479 _moduleLogger.info("Cache saved")
480 except (pickle.PickleError, IOError):
481 _moduleLogger.exception("While saving")
483 def _clear_cache(self):
484 updateContacts = len(self._contacts) != 0
485 updateMessages = len(self._messages) != 0
486 updateHistory = len(self._history) != 0
488 oldCallback = self._callback
491 self._contactUpdateTime = datetime.datetime(1, 1, 1)
493 self._messageUpdateTime = datetime.datetime(1, 1, 1)
495 self._historyUpdateTime = datetime.datetime(1, 1, 1)
500 self.contactsUpdated.emit()
502 self.messagesUpdated.emit()
504 self.historyUpdated.emit()
505 if oldDnd != self._dnd:
506 self.dndStateChange.emit(self._dnd)
507 if oldCallback != self._callback:
508 self.callbackNumberChanged.emit(self._callback)
510 self._save_to_cache()
512 def _update_contacts(self):
514 self._contacts = yield (
515 self._backend[0].get_contacts,
520 self.error.emit(str(e))
522 self._contactUpdateTime = datetime.datetime.now()
523 self.contactsUpdated.emit()
525 def _update_messages(self):
527 self._messages = yield (
528 self._backend[0].get_messages,
533 self.error.emit(str(e))
535 self._messageUpdateTime = datetime.datetime.now()
536 self.messagesUpdated.emit()
538 def _update_history(self):
540 self._history = yield (
541 self._backend[0].get_recent,
546 self.error.emit(str(e))
548 self._historyUpdateTime = datetime.datetime.now()
549 self.historyUpdated.emit()
551 def _update_dnd(self):
555 self._backend[0].is_dnd,
560 self.error.emit(str(e))
562 if oldDnd != self._dnd:
563 self.dndStateChange(self._dnd)
565 def _perform_op_while_loggedin(self, op):
566 if self.state == self.LOGGEDIN_STATE:
569 self._push_login_op(op)
571 def _push_login_op(self, asyncOp):
572 assert self.state != self.LOGGEDIN_STATE
573 if asyncOp in self._loginOps:
574 _moduleLogger.info("Skipping queueing duplicate op: %r" % asyncOp)
576 self._loginOps.append(asyncOp)