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