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 = [misc_utils.make_ugly(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 # @todo Somehow add support for csv contacts
157 stateChange = QtCore.pyqtSignal(str)
158 loggedOut = QtCore.pyqtSignal()
159 loggedIn = QtCore.pyqtSignal()
160 callbackNumberChanged = QtCore.pyqtSignal(str)
162 contactsUpdated = QtCore.pyqtSignal()
163 messagesUpdated = QtCore.pyqtSignal()
164 historyUpdated = QtCore.pyqtSignal()
165 dndStateChange = QtCore.pyqtSignal(bool)
167 error = QtCore.pyqtSignal(str)
169 LOGGEDOUT_STATE = "logged out"
170 LOGGINGIN_STATE = "logging in"
171 LOGGEDIN_STATE = "logged in"
173 _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.1.90")
178 def __init__(self, cachePath = None):
179 QtCore.QObject.__init__(self)
180 self._pool = qore_utils.AsyncPool()
182 self._loggedInTime = self._LOGGEDOUT_TIME
184 self._cachePath = cachePath
185 self._username = None
186 self._draft = Draft(self._pool, self._backend)
189 self._contactUpdateTime = datetime.datetime(1971, 1, 1)
191 self._messageUpdateTime = datetime.datetime(1971, 1, 1)
193 self._historyUpdateTime = datetime.datetime(1971, 1, 1)
200 self._LOGGEDOUT_TIME: self.LOGGEDOUT_STATE,
201 self._LOGGINGIN_TIME: self.LOGGINGIN_STATE,
202 }.get(self._loggedInTime, self.LOGGEDIN_STATE)
208 def login(self, username, password):
209 assert self.state == self.LOGGEDOUT_STATE
210 assert username != ""
211 if self._cachePath is not None:
212 cookiePath = os.path.join(self._cachePath, "%s.cookies" % username)
216 if self._username != username or not self._backend:
217 from backends import gv_backend
219 self._backend[0:0] = [gv_backend.GVDialer(cookiePath)]
222 le = concurrent.AsyncLinearExecution(self._pool, self._login)
223 le.start(username, password)
226 assert self.state != self.LOGGEDOUT_STATE
228 self._loggedInTime = self._LOGGEDOUT_TIME
229 self._backend[0].persist()
230 self._save_to_cache()
233 assert self.state == self.LOGGEDOUT_STATE
234 self._backend[0].logout()
239 def logout_and_clear(self):
240 assert self.state != self.LOGGEDOUT_STATE
242 self._loggedInTime = self._LOGGEDOUT_TIME
245 def update_contacts(self, force = True):
246 if not force and self._contacts:
248 le = concurrent.AsyncLinearExecution(self._pool, self._update_contacts)
249 self._perform_op_while_loggedin(le)
251 def get_contacts(self):
252 return self._contacts
254 def get_when_contacts_updated(self):
255 return self._contactUpdateTime
257 def update_messages(self, force = True):
258 if not force and self._messages:
260 le = concurrent.AsyncLinearExecution(self._pool, self._update_messages)
261 self._perform_op_while_loggedin(le)
263 def get_messages(self):
264 return self._messages
266 def get_when_messages_updated(self):
267 return self._messageUpdateTime
269 def update_history(self, force = True):
270 if not force and self._history:
272 le = concurrent.AsyncLinearExecution(self._pool, self._update_history)
273 self._perform_op_while_loggedin(le)
275 def get_history(self):
278 def get_when_history_updated(self):
279 return self._historyUpdateTime
281 def update_dnd(self):
282 le = concurrent.AsyncLinearExecution(self._pool, self._update_dnd)
283 self._perform_op_while_loggedin(le)
285 def set_dnd(self, dnd):
286 le = concurrent.AsyncLinearExecution(self._pool, self._set_dnd)
289 def _set_dnd(self, dnd):
290 # I'm paranoid about our state geting out of sync so we set no matter
291 # what but act as if we have the cannonical state
292 assert self.state == self.LOGGEDIN_STATE
296 self._backend[0].set_dnd,
301 self.error.emit(str(e))
304 if oldDnd != self._dnd:
305 self.dndStateChange.emit(self._dnd)
310 def get_account_number(self):
311 return self._backend[0].get_account_number()
313 def get_callback_numbers(self):
314 # @todo Remove evilness (might call is_authed which can block)
315 return self._backend[0].get_callback_numbers()
317 def get_callback_number(self):
318 return self._callback
320 def set_callback_number(self, callback):
321 le = concurrent.AsyncLinearExecution(self._pool, self._set_callback_number)
324 def _set_callback_number(self, callback):
325 # I'm paranoid about our state geting out of sync so we set no matter
326 # what but act as if we have the cannonical state
327 assert self.state == self.LOGGEDIN_STATE
328 oldCallback = self._callback
331 self._backend[0].set_callback_number,
336 self.error.emit(str(e))
338 self._callback = callback
339 if oldCallback != self._callback:
340 self.callbackNumberChanged.emit(self._callback)
342 def _login(self, username, password):
343 self._loggedInTime = self._LOGGINGIN_TIME
344 self.stateChange.emit(self.LOGGINGIN_STATE)
345 finalState = self.LOGGEDOUT_STATE
349 if not isLoggedIn and self._backend[0].is_quick_login_possible():
351 self._backend[0].is_authed,
356 _moduleLogger.info("Logged in through cookies")
358 # Force a clearing of the cookies
360 self._backend[0].logout,
367 self._backend[0].login,
368 (username, password),
372 _moduleLogger.info("Logged in through credentials")
375 self._loggedInTime = int(time.time())
376 oldUsername = self._username
377 self._username = username
378 finalState = self.LOGGEDIN_STATE
380 if oldUsername != self._username:
381 needOps = not self._load()
385 loginOps = self._loginOps[:]
388 del self._loginOps[:]
389 for asyncOp in loginOps:
392 self.error.emit(str(e))
394 self.stateChange.emit(finalState)
397 updateContacts = len(self._contacts) != 0
398 updateMessages = len(self._messages) != 0
399 updateHistory = len(self._history) != 0
401 oldCallback = self._callback
409 loadedFromCache = self._load_from_cache()
411 updateContacts = True
412 updateMessages = True
416 self.contactsUpdated.emit()
418 self.messagesUpdated.emit()
420 self.historyUpdated.emit()
421 if oldDnd != self._dnd:
422 self.dndStateChange.emit(self._dnd)
423 if oldCallback != self._callback:
424 self.callbackNumberChanged.emit(self._callback)
426 return loadedFromCache
428 def _load_from_cache(self):
429 if self._cachePath is None:
431 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
434 with open(cachePath, "rb") as f:
435 dumpedData = pickle.load(f)
436 except (pickle.PickleError, IOError, EOFError, ValueError):
437 _moduleLogger.exception("Pickle fun loading")
440 _moduleLogger.exception("Weirdness loading")
445 contacts, contactUpdateTime,
446 messages, messageUpdateTime,
447 history, historyUpdateTime,
451 if misc_utils.compare_versions(
452 self._OLDEST_COMPATIBLE_FORMAT_VERSION,
453 misc_utils.parse_version(version),
455 _moduleLogger.info("Loaded cache")
456 self._contacts = contacts
457 self._contactUpdateTime = contactUpdateTime
458 self._messages = messages
459 self._messageUpdateTime = messageUpdateTime
460 self._history = history
461 self._historyUpdateTime = historyUpdateTime
463 self._callback = callback
467 "Skipping cache due to version mismatch (%s-%s)" % (
473 def _save_to_cache(self):
474 _moduleLogger.info("Saving cache")
475 if self._cachePath is None:
477 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
481 constants.__version__, constants.__build__,
482 self._contacts, self._contactUpdateTime,
483 self._messages, self._messageUpdateTime,
484 self._history, self._historyUpdateTime,
485 self._dnd, self._callback
487 with open(cachePath, "wb") as f:
488 pickle.dump(dataToDump, f, pickle.HIGHEST_PROTOCOL)
489 _moduleLogger.info("Cache saved")
490 except (pickle.PickleError, IOError):
491 _moduleLogger.exception("While saving")
493 def _clear_cache(self):
494 updateContacts = len(self._contacts) != 0
495 updateMessages = len(self._messages) != 0
496 updateHistory = len(self._history) != 0
498 oldCallback = self._callback
501 self._contactUpdateTime = datetime.datetime(1, 1, 1)
503 self._messageUpdateTime = datetime.datetime(1, 1, 1)
505 self._historyUpdateTime = datetime.datetime(1, 1, 1)
510 self.contactsUpdated.emit()
512 self.messagesUpdated.emit()
514 self.historyUpdated.emit()
515 if oldDnd != self._dnd:
516 self.dndStateChange.emit(self._dnd)
517 if oldCallback != self._callback:
518 self.callbackNumberChanged.emit(self._callback)
520 self._save_to_cache()
522 def _update_contacts(self):
524 self._contacts = yield (
525 self._backend[0].get_contacts,
530 self.error.emit(str(e))
532 self._contactUpdateTime = datetime.datetime.now()
533 self.contactsUpdated.emit()
535 def _update_messages(self):
537 self._messages = yield (
538 self._backend[0].get_messages,
543 self.error.emit(str(e))
545 self._messageUpdateTime = datetime.datetime.now()
546 self.messagesUpdated.emit()
548 def _update_history(self):
550 self._history = yield (
551 self._backend[0].get_recent,
556 self.error.emit(str(e))
558 self._historyUpdateTime = datetime.datetime.now()
559 self.historyUpdated.emit()
561 def _update_dnd(self):
565 self._backend[0].is_dnd,
570 self.error.emit(str(e))
572 if oldDnd != self._dnd:
573 self.dndStateChange(self._dnd)
575 def _perform_op_while_loggedin(self, op):
576 if self.state == self.LOGGEDIN_STATE:
579 self._push_login_op(op)
581 def _push_login_op(self, asyncOp):
582 assert self.state != self.LOGGEDIN_STATE
583 if asyncOp in self._loginOps:
584 _moduleLogger.info("Skipping queueing duplicate op: %r" % asyncOp)
586 self._loginOps.append(asyncOp)