Implementing caches for a session
[gc-dialer] / src / session.py
1 import os
2 import time
3 import logging
4
5 try:
6         import cPickle
7         pickle = cPickle
8 except ImportError:
9         import pickle
10
11 from PyQt4 import QtCore
12
13 from util import qore_utils
14 from util import concurrent
15 from util import misc as misc_utils
16
17 import constants
18 from backends import gv_backend
19
20
21 _moduleLogger = logging.getLogger(__name__)
22
23
24 class _DraftContact(object):
25
26         def __init__(self, title, description, numbersWithDescriptions):
27                 self.title = title
28                 self.description = description
29                 self.numbers = numbersWithDescriptions
30                 self.selectedNumber = numbersWithDescriptions[0][0]
31
32
33 class Draft(QtCore.QObject):
34
35         sendingMessage = QtCore.pyqtSignal()
36         sentMessage = QtCore.pyqtSignal()
37         calling = QtCore.pyqtSignal()
38         called = QtCore.pyqtSignal()
39         cancelling = QtCore.pyqtSignal()
40         cancelled = QtCore.pyqtSignal()
41         error = QtCore.pyqtSignal(str)
42
43         recipientsChanged = QtCore.pyqtSignal()
44
45         def __init__(self, pool):
46                 QtCore.QObject.__init__(self)
47                 self._contacts = {}
48                 self._pool = pool
49
50         def send(self, text):
51                 assert 0 < len(self._contacts)
52                 numbers = [contact.selectedNumber for contact in self._contacts.itervalues()]
53                 le = concurrent.AsyncLinearExecution(self._pool, self._send)
54                 le.start(numbers, text)
55
56         def call(self):
57                 assert len(self._contacts) == 1
58                 (contact, ) = self._contacts.itervalues()
59                 le = concurrent.AsyncLinearExecution(self._pool, self._call)
60                 le.start(contact.selectedNumber)
61
62         def cancel(self):
63                 le = concurrent.AsyncLinearExecution(self._pool, self._cancel)
64                 le.start()
65
66         def add_contact(self, contactId, title, description, numbersWithDescriptions):
67                 assert contactId not in self._contacts
68                 contactDetails = _DraftContact(title, description, numbersWithDescriptions)
69                 self._contacts[contactId] = contactDetails
70                 self.recipientsChanged.emit()
71
72         def remove_contact(self, contactId):
73                 assert contactId in self._contacts
74                 del self._contacts[contactId]
75                 self.recipientsChanged.emit()
76
77         def get_contacts(self):
78                 return self._contacts.iterkeys()
79
80         def get_num_contacts(self):
81                 return len(self._contacts)
82
83         def get_title(self, cid):
84                 return self._contacts[cid].title
85
86         def get_description(self, cid):
87                 return self._contacts[cid].description
88
89         def get_numbers(self, cid):
90                 return self._contacts[cid].numbers
91
92         def get_selected_number(self, cid):
93                 return self._contacts[cid].selectedNumber
94
95         def set_selected_number(self, cid, number):
96                 # @note I'm lazy, this isn't firing any kind of signal since only one
97                 # controller right now and that is the viewer
98                 return self._contacts[cid].numbers
99
100         def clear(self):
101                 oldContacts = self._contacts
102                 self._contacts = {}
103                 if oldContacts:
104                         self.recipientsChanged.emit()
105
106         def _send(self, numbers, text):
107                 self.sendingMessage.emit()
108                 try:
109                         self.error.emit("Not Implemented")
110                         self.sentMessage.emit()
111                         self.clear()
112                 except Exception, e:
113                         self.error.emit(str(e))
114
115         def _call(self, number):
116                 self.calling.emit()
117                 try:
118                         self.error.emit("Not Implemented")
119                         self.called.emit()
120                         self.clear()
121                 except Exception, e:
122                         self.error.emit(str(e))
123
124         def _cancel(self):
125                 self.cancelling.emit()
126                 try:
127                         yield (
128                                 self._backend.cancel,
129                                 (),
130                                 {},
131                         )
132                         self.cancelled.emit()
133                 except Exception, e:
134                         self.error.emit(str(e))
135
136
137 class Session(QtCore.QObject):
138
139         stateChange = QtCore.pyqtSignal(str)
140         loggedOut = QtCore.pyqtSignal()
141         loggedIn = QtCore.pyqtSignal()
142         callbackNumberChanged = QtCore.pyqtSignal(str)
143
144         contactsUpdated = QtCore.pyqtSignal()
145         messagesUpdated = QtCore.pyqtSignal()
146         historyUpdated = QtCore.pyqtSignal()
147         dndStateChange = QtCore.pyqtSignal(bool)
148
149         error = QtCore.pyqtSignal(str)
150
151         LOGGEDOUT_STATE = "logged out"
152         LOGGINGIN_STATE = "logging in"
153         LOGGEDIN_STATE = "logged in"
154
155         _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.2.0")
156
157         _LOGGEDOUT_TIME = -1
158         _LOGGINGIN_TIME = 0
159
160         def __init__(self, cachePath = None):
161                 QtCore.QObject.__init__(self)
162                 self._pool = qore_utils.AsyncPool()
163                 self._backend = None
164                 self._loggedInTime = self._LOGGEDOUT_TIME
165                 self._loginOps = []
166                 self._cachePath = cachePath
167                 self._username = None
168                 self._draft = Draft(self._pool)
169
170                 self._contacts = {}
171                 self._messages = []
172                 self._history = []
173                 self._dnd = False
174                 self._callback = ""
175
176         @property
177         def state(self):
178                 return {
179                         self._LOGGEDOUT_TIME: self.LOGGEDOUT_STATE,
180                         self._LOGGINGIN_TIME: self.LOGGINGIN_STATE,
181                 }.get(self._loggedInTime, self.LOGGEDIN_STATE)
182
183         @property
184         def draft(self):
185                 return self._draft
186
187         def login(self, username, password):
188                 assert self.state == self.LOGGEDOUT_STATE
189                 assert username != ""
190                 if self._cachePath is not None:
191                         cookiePath = os.path.join(self._cachePath, "%s.cookies" % username)
192                 else:
193                         cookiePath = None
194
195                 if self._username != username or self._backend is None:
196                         self._backend = gv_backend.GVDialer(cookiePath)
197
198                 self._pool.start()
199                 le = concurrent.AsyncLinearExecution(self._pool, self._login)
200                 le.start(username, password)
201
202         def logout(self):
203                 assert self.state != self.LOGGEDOUT_STATE
204                 self._pool.stop()
205                 self._loggedInTime = self._LOGGEDOUT_TIME
206                 self._backend.persist()
207                 self._save_to_cache()
208
209         def clear(self):
210                 assert self.state == self.LOGGEDOUT_STATE
211                 self._backend.logout()
212                 self._backend = None
213                 self._clear_cache()
214                 self._draft.clear()
215
216         def logout_and_clear(self):
217                 assert self.state != self.LOGGEDOUT_STATE
218                 self._pool.stop()
219                 self._loggedInTime = self._LOGGEDOUT_TIME
220                 self.clear()
221
222         def update_contacts(self, force = True):
223                 if not force and self._contacts:
224                         return
225                 le = concurrent.AsyncLinearExecution(self._pool, self._update_contacts)
226                 self._perform_op_while_loggedin(le)
227
228         def get_contacts(self):
229                 return self._contacts
230
231         def update_messages(self, force = True):
232                 if not force and self._messages:
233                         return
234                 le = concurrent.AsyncLinearExecution(self._pool, self._update_messages)
235                 self._perform_op_while_loggedin(le)
236
237         def get_messages(self):
238                 return self._messages
239
240         def update_history(self, force = True):
241                 if not force and self._history:
242                         return
243                 le = concurrent.AsyncLinearExecution(self._pool, self._update_history)
244                 self._perform_op_while_loggedin(le)
245
246         def get_history(self):
247                 return self._history
248
249         def update_dnd(self):
250                 le = concurrent.AsyncLinearExecution(self._pool, self._update_dnd)
251                 self._perform_op_while_loggedin(le)
252
253         def set_dnd(self, dnd):
254                 # I'm paranoid about our state geting out of sync so we set no matter
255                 # what but act as if we have the cannonical state
256                 assert self.state == self.LOGGEDIN_STATE
257                 oldDnd = self._dnd
258                 try:
259                         yield (
260                                 self._backend.set_dnd,
261                                 (dnd),
262                                 {},
263                         )
264                 except Exception, e:
265                         self.error.emit(str(e))
266                         return
267                 self._dnd = dnd
268                 if oldDnd != self._dnd:
269                         self.dndStateChange.emit(self._dnd)
270
271         def get_dnd(self):
272                 return self._dnd
273
274         def get_callback_numbers(self):
275                 # @todo Remove evilness
276                 return self._backend.get_callback_numbers()
277
278         def get_callback_number(self):
279                 return self._callback
280
281         def set_callback_number(self, callback):
282                 # I'm paranoid about our state geting out of sync so we set no matter
283                 # what but act as if we have the cannonical state
284                 assert self.state == self.LOGGEDIN_STATE
285                 oldCallback = self._callback
286                 try:
287                         yield (
288                                 self._backend.set_callback_number,
289                                 (callback),
290                                 {},
291                         )
292                 except Exception, e:
293                         self.error.emit(str(e))
294                         return
295                 self._callback = callback
296                 if oldCallback != self._callback:
297                         self.callbackNumberChanged.emit(self._callback)
298
299         def _login(self, username, password):
300                 self._loggedInTime = self._LOGGINGIN_TIME
301                 self.stateChange.emit(self.LOGGINGIN_STATE)
302                 finalState = self.LOGGEDOUT_STATE
303                 try:
304                         isLoggedIn = False
305
306                         if not isLoggedIn and self._backend.is_quick_login_possible():
307                                 isLoggedIn = yield (
308                                         self._backend.is_authed,
309                                         (),
310                                         {},
311                                 )
312                                 if isLoggedIn:
313                                         _moduleLogger.info("Logged in through cookies")
314                                 else:
315                                         # Force a clearing of the cookies
316                                         yield (
317                                                 self._backend.logout,
318                                                 (),
319                                                 {},
320                                         )
321
322                         if not isLoggedIn:
323                                 isLoggedIn = yield (
324                                         self._backend.login,
325                                         (username, password),
326                                         {},
327                                 )
328                                 if isLoggedIn:
329                                         _moduleLogger.info("Logged in through credentials")
330
331                         if isLoggedIn:
332                                 self._loggedInTime = int(time.time())
333                                 oldUsername = self._username
334                                 self._username = username
335                                 finalState = self.LOGGEDIN_STATE
336                                 self.loggedIn.emit()
337                                 if oldUsername != self._username:
338                                         needOps = not self._load()
339                                 else:
340                                         needOps = True
341                                 if needOps:
342                                         loginOps = self._loginOps[:]
343                                 else:
344                                         loginOps = []
345                                 del self._loginOps[:]
346                                 for asyncOp in loginOps:
347                                         asyncOp.start()
348                 except Exception, e:
349                         self.error.emit(str(e))
350                 finally:
351                         self.stateChange.emit(finalState)
352
353         def _load(self):
354                 updateContacts = len(self._contacts) != 0
355                 updateMessages = len(self._messages) != 0
356                 updateHistory = len(self._history) != 0
357                 oldDnd = self._dnd
358                 oldCallback = self._callback
359
360                 self._contacts = {}
361                 self._messages = []
362                 self._history = []
363                 self._dnd = False
364                 self._callback = ""
365
366                 loadedFromCache = self._load_from_cache()
367                 if loadedFromCache:
368                         updateContacts = True
369                         updateMessages = True
370                         updateHistory = True
371
372                 if updateContacts:
373                         self.contactsUpdated.emit()
374                 if updateMessages:
375                         self.messagesUpdated.emit()
376                 if updateHistory:
377                         self.historyUpdated.emit()
378                 if oldDnd != self._dnd:
379                         self.dndStateChange.emit(self._dnd)
380                 if oldCallback != self._callback:
381                         self.callbackNumberChanged.emit(self._callback)
382
383                 return loadedFromCache
384
385         def _load_from_cache(self):
386                 if self._cachePath is None:
387                         return False
388                 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
389
390                 try:
391                         with open(cachePath, "rb") as f:
392                                 dumpedData = pickle.load(f)
393                 except (pickle.PickleError, IOError, EOFError, ValueError):
394                         _moduleLogger.exception("Pickle fun loading")
395                         return False
396                 except:
397                         _moduleLogger.exception("Weirdness loading")
398                         return False
399
400                 (
401                         version, build,
402                         contacts, messages, history, dnd, callback
403                 ) = dumpedData
404
405                 if misc_utils.compare_versions(
406                         self._OLDEST_COMPATIBLE_FORMAT_VERSION,
407                         misc_utils.parse_version(version),
408                 ) <= 0:
409                         _moduleLogger.info("Loaded cache")
410                         self._contacts = contacts
411                         self._messages = messages
412                         self._history = history
413                         self._dnd = dnd
414                         self._callback = callback
415                         return True
416                 else:
417                         _moduleLogger.debug(
418                                 "Skipping cache due to version mismatch (%s-%s)" % (
419                                         version, build
420                                 )
421                         )
422                         return False
423
424         def _save_to_cache(self):
425                 _moduleLogger.info("Saving cache")
426                 if self._cachePath is None:
427                         return
428                 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
429
430                 try:
431                         dataToDump = (
432                                 constants.__version__, constants.__build__,
433                                 self._contacts, self._messages, self._history, self._dnd, self._callback
434                         )
435                         with open(cachePath, "wb") as f:
436                                 pickle.dump(dataToDump, f, pickle.HIGHEST_PROTOCOL)
437                         _moduleLogger.info("Cache saved")
438                 except (pickle.PickleError, IOError):
439                         _moduleLogger.exception("While saving")
440
441         def _clear_cache(self):
442                 updateContacts = len(self._contacts) != 0
443                 updateMessages = len(self._messages) != 0
444                 updateHistory = len(self._history) != 0
445                 oldDnd = self._dnd
446                 oldCallback = self._callback
447
448                 self._contacts = {}
449                 self._messages = []
450                 self._history = []
451                 self._dnd = False
452                 self._callback = ""
453
454                 if updateContacts:
455                         self.contactsUpdated.emit()
456                 if updateMessages:
457                         self.messagesUpdated.emit()
458                 if updateHistory:
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)
464
465                 self._save_to_cache()
466
467         def _update_contacts(self):
468                 try:
469                         self._contacts = yield (
470                                 self._backend.get_contacts,
471                                 (),
472                                 {},
473                         )
474                 except Exception, e:
475                         self.error.emit(str(e))
476                         return
477                 self.contactsUpdated.emit()
478
479         def _update_messages(self):
480                 try:
481                         self._messages = yield (
482                                 self._backend.get_messages,
483                                 (),
484                                 {},
485                         )
486                 except Exception, e:
487                         self.error.emit(str(e))
488                         return
489                 self.messagesUpdated.emit()
490
491         def _update_history(self):
492                 try:
493                         self._history = yield (
494                                 self._backend.get_recent,
495                                 (),
496                                 {},
497                         )
498                 except Exception, e:
499                         self.error.emit(str(e))
500                         return
501                 self.historyUpdated.emit()
502
503         def _update_dnd(self):
504                 oldDnd = self._dnd
505                 try:
506                         self._dnd = yield (
507                                 self._backend.is_dnd,
508                                 (),
509                                 {},
510                         )
511                 except Exception, e:
512                         self.error.emit(str(e))
513                         return
514                 if oldDnd != self._dnd:
515                         self.dndStateChange(self._dnd)
516
517         def _perform_op_while_loggedin(self, op):
518                 if self.state == self.LOGGEDIN_STATE:
519                         op.start()
520                 else:
521                         self._push_login_op(op)
522
523         def _push_login_op(self, asyncOp):
524                 assert self.state != self.LOGGEDIN_STATE
525                 if asyncOp in self._loginOps:
526                         _moduleLogger.info("Skipping queueing duplicate op: %r" % asyncOp)
527                         return
528                 self._loginOps.append(asyncOp)