39c146cb2194d7994f2649460e829527e149e5d2
[gc-dialer] / src / session.py
1 from __future__ import with_statement
2
3 import os
4 import time
5 import datetime
6 import logging
7
8 try:
9         import cPickle
10         pickle = cPickle
11 except ImportError:
12         import pickle
13
14 from PyQt4 import QtCore
15
16 from util import qore_utils
17 from util import concurrent
18 from util import misc as misc_utils
19
20 import constants
21
22
23 _moduleLogger = logging.getLogger(__name__)
24
25
26 class _DraftContact(object):
27
28         def __init__(self, title, description, numbersWithDescriptions):
29                 self.title = title
30                 self.description = description
31                 self.numbers = numbersWithDescriptions
32                 self.selectedNumber = numbersWithDescriptions[0][0]
33
34
35 class Draft(QtCore.QObject):
36
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)
44
45         recipientsChanged = QtCore.pyqtSignal()
46
47         def __init__(self, pool, backend):
48                 QtCore.QObject.__init__(self)
49                 self._contacts = {}
50                 self._pool = pool
51                 self._backend = backend
52
53         def send(self, text):
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)
58
59         def call(self):
60                 assert len(self._contacts) == 1
61                 (contact, ) = self._contacts.itervalues()
62                 le = concurrent.AsyncLinearExecution(self._pool, self._call)
63                 le.start(contact.selectedNumber)
64
65         def cancel(self):
66                 le = concurrent.AsyncLinearExecution(self._pool, self._cancel)
67                 le.start()
68
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()
74                         return
75                 contactDetails = _DraftContact(title, description, numbersWithDescriptions)
76                 self._contacts[contactId] = contactDetails
77                 self.recipientsChanged.emit()
78
79         def remove_contact(self, contactId):
80                 assert contactId in self._contacts
81                 del self._contacts[contactId]
82                 self.recipientsChanged.emit()
83
84         def get_contacts(self):
85                 return self._contacts.iterkeys()
86
87         def get_num_contacts(self):
88                 return len(self._contacts)
89
90         def get_title(self, cid):
91                 return self._contacts[cid].title
92
93         def get_description(self, cid):
94                 return self._contacts[cid].description
95
96         def get_numbers(self, cid):
97                 return self._contacts[cid].numbers
98
99         def get_selected_number(self, cid):
100                 return self._contacts[cid].selectedNumber
101
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
107
108         def clear(self):
109                 oldContacts = self._contacts
110                 self._contacts = {}
111                 if oldContacts:
112                         self.recipientsChanged.emit()
113
114         def _send(self, numbers, text):
115                 self.sendingMessage.emit()
116                 try:
117                         yield (
118                                 self._backend[0].send_sms,
119                                 (numbers, text),
120                                 {},
121                         )
122                         self.sentMessage.emit()
123                         self.clear()
124                 except Exception, e:
125                         self.error.emit(str(e))
126
127         def _call(self, number):
128                 self.calling.emit()
129                 try:
130                         yield (
131                                 self._backend[0].call,
132                                 (number, ),
133                                 {},
134                         )
135                         self.called.emit()
136                         self.clear()
137                 except Exception, e:
138                         self.error.emit(str(e))
139
140         def _cancel(self):
141                 self.cancelling.emit()
142                 try:
143                         yield (
144                                 self._backend[0].cancel,
145                                 (),
146                                 {},
147                         )
148                         self.cancelled.emit()
149                 except Exception, e:
150                         self.error.emit(str(e))
151
152
153 class Session(QtCore.QObject):
154
155         stateChange = QtCore.pyqtSignal(str)
156         loggedOut = QtCore.pyqtSignal()
157         loggedIn = QtCore.pyqtSignal()
158         callbackNumberChanged = QtCore.pyqtSignal(str)
159
160         contactsUpdated = QtCore.pyqtSignal()
161         messagesUpdated = QtCore.pyqtSignal()
162         historyUpdated = QtCore.pyqtSignal()
163         dndStateChange = QtCore.pyqtSignal(bool)
164
165         error = QtCore.pyqtSignal(str)
166
167         LOGGEDOUT_STATE = "logged out"
168         LOGGINGIN_STATE = "logging in"
169         LOGGEDIN_STATE = "logged in"
170
171         _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.2.0")
172
173         _LOGGEDOUT_TIME = -1
174         _LOGGINGIN_TIME = 0
175
176         def __init__(self, cachePath = None):
177                 QtCore.QObject.__init__(self)
178                 self._pool = qore_utils.AsyncPool()
179                 self._backend = []
180                 self._loggedInTime = self._LOGGEDOUT_TIME
181                 self._loginOps = []
182                 self._cachePath = cachePath
183                 self._username = None
184                 self._draft = Draft(self._pool, self._backend)
185
186                 self._contacts = {}
187                 self._contactUpdateTime = datetime.datetime(1971, 1, 1)
188                 self._messages = []
189                 self._messageUpdateTime = datetime.datetime(1971, 1, 1)
190                 self._history = []
191                 self._historyUpdateTime = datetime.datetime(1971, 1, 1)
192                 self._dnd = False
193                 self._callback = ""
194
195         @property
196         def state(self):
197                 return {
198                         self._LOGGEDOUT_TIME: self.LOGGEDOUT_STATE,
199                         self._LOGGINGIN_TIME: self.LOGGINGIN_STATE,
200                 }.get(self._loggedInTime, self.LOGGEDIN_STATE)
201
202         @property
203         def draft(self):
204                 return self._draft
205
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)
211                 else:
212                         cookiePath = None
213
214                 if self._username != username or not self._backend:
215                         from backends import gv_backend
216                         del self._backend[:]
217                         self._backend[0:0] = [gv_backend.GVDialer(cookiePath)]
218
219                 self._pool.start()
220                 le = concurrent.AsyncLinearExecution(self._pool, self._login)
221                 le.start(username, password)
222
223         def logout(self):
224                 assert self.state != self.LOGGEDOUT_STATE
225                 self._pool.stop()
226                 self._loggedInTime = self._LOGGEDOUT_TIME
227                 self._backend[0].persist()
228                 self._save_to_cache()
229
230         def clear(self):
231                 assert self.state == self.LOGGEDOUT_STATE
232                 self._backend[0].logout()
233                 del self._backend[0]
234                 self._clear_cache()
235                 self._draft.clear()
236
237         def logout_and_clear(self):
238                 assert self.state != self.LOGGEDOUT_STATE
239                 self._pool.stop()
240                 self._loggedInTime = self._LOGGEDOUT_TIME
241                 self.clear()
242
243         def update_contacts(self, force = True):
244                 if not force and self._contacts:
245                         return
246                 le = concurrent.AsyncLinearExecution(self._pool, self._update_contacts)
247                 self._perform_op_while_loggedin(le)
248
249         def get_contacts(self):
250                 return self._contacts
251
252         def get_when_contacts_updated(self):
253                 return self._contactUpdateTime
254
255         def update_messages(self, force = True):
256                 if not force and self._messages:
257                         return
258                 le = concurrent.AsyncLinearExecution(self._pool, self._update_messages)
259                 self._perform_op_while_loggedin(le)
260
261         def get_messages(self):
262                 return self._messages
263
264         def get_when_messages_updated(self):
265                 return self._messageUpdateTime
266
267         def update_history(self, force = True):
268                 if not force and self._history:
269                         return
270                 le = concurrent.AsyncLinearExecution(self._pool, self._update_history)
271                 self._perform_op_while_loggedin(le)
272
273         def get_history(self):
274                 return self._history
275
276         def get_when_history_updated(self):
277                 return self._historyUpdateTime
278
279         def update_dnd(self):
280                 le = concurrent.AsyncLinearExecution(self._pool, self._update_dnd)
281                 self._perform_op_while_loggedin(le)
282
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
287                 oldDnd = self._dnd
288                 try:
289                         yield (
290                                 self._backend[0].set_dnd,
291                                 (dnd),
292                                 {},
293                         )
294                 except Exception, e:
295                         self.error.emit(str(e))
296                         return
297                 self._dnd = dnd
298                 if oldDnd != self._dnd:
299                         self.dndStateChange.emit(self._dnd)
300
301         def get_dnd(self):
302                 return self._dnd
303
304         def get_account_number(self):
305                 return self._backend[0].get_account_number()
306
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()
310
311         def get_callback_number(self):
312                 return self._callback
313
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
319                 try:
320                         yield (
321                                 self._backend[0].set_callback_number,
322                                 (callback),
323                                 {},
324                         )
325                 except Exception, e:
326                         self.error.emit(str(e))
327                         return
328                 self._callback = callback
329                 if oldCallback != self._callback:
330                         self.callbackNumberChanged.emit(self._callback)
331
332         def _login(self, username, password):
333                 self._loggedInTime = self._LOGGINGIN_TIME
334                 self.stateChange.emit(self.LOGGINGIN_STATE)
335                 finalState = self.LOGGEDOUT_STATE
336                 try:
337                         isLoggedIn = False
338
339                         if not isLoggedIn and self._backend[0].is_quick_login_possible():
340                                 isLoggedIn = yield (
341                                         self._backend[0].is_authed,
342                                         (),
343                                         {},
344                                 )
345                                 if isLoggedIn:
346                                         _moduleLogger.info("Logged in through cookies")
347                                 else:
348                                         # Force a clearing of the cookies
349                                         yield (
350                                                 self._backend[0].logout,
351                                                 (),
352                                                 {},
353                                         )
354
355                         if not isLoggedIn:
356                                 isLoggedIn = yield (
357                                         self._backend[0].login,
358                                         (username, password),
359                                         {},
360                                 )
361                                 if isLoggedIn:
362                                         _moduleLogger.info("Logged in through credentials")
363
364                         if isLoggedIn:
365                                 self._loggedInTime = int(time.time())
366                                 oldUsername = self._username
367                                 self._username = username
368                                 finalState = self.LOGGEDIN_STATE
369                                 self.loggedIn.emit()
370                                 if oldUsername != self._username:
371                                         needOps = not self._load()
372                                 else:
373                                         needOps = True
374                                 if needOps:
375                                         loginOps = self._loginOps[:]
376                                 else:
377                                         loginOps = []
378                                 del self._loginOps[:]
379                                 for asyncOp in loginOps:
380                                         asyncOp.start()
381                 except Exception, e:
382                         self.error.emit(str(e))
383                 finally:
384                         self.stateChange.emit(finalState)
385
386         def _load(self):
387                 updateContacts = len(self._contacts) != 0
388                 updateMessages = len(self._messages) != 0
389                 updateHistory = len(self._history) != 0
390                 oldDnd = self._dnd
391                 oldCallback = self._callback
392
393                 self._contacts = {}
394                 self._messages = []
395                 self._history = []
396                 self._dnd = False
397                 self._callback = ""
398
399                 loadedFromCache = self._load_from_cache()
400                 if loadedFromCache:
401                         updateContacts = True
402                         updateMessages = True
403                         updateHistory = True
404
405                 if updateContacts:
406                         self.contactsUpdated.emit()
407                 if updateMessages:
408                         self.messagesUpdated.emit()
409                 if updateHistory:
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)
415
416                 return loadedFromCache
417
418         def _load_from_cache(self):
419                 if self._cachePath is None:
420                         return False
421                 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
422
423                 try:
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")
428                         return False
429                 except:
430                         _moduleLogger.exception("Weirdness loading")
431                         return False
432
433                 (
434                         version, build,
435                         contacts, contactUpdateTime,
436                         messages, messageUpdateTime,
437                         history, historyUpdateTime,
438                         dnd, callback
439                 ) = dumpedData
440
441                 if misc_utils.compare_versions(
442                         self._OLDEST_COMPATIBLE_FORMAT_VERSION,
443                         misc_utils.parse_version(version),
444                 ) <= 0:
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
452                         self._dnd = dnd
453                         self._callback = callback
454                         return True
455                 else:
456                         _moduleLogger.debug(
457                                 "Skipping cache due to version mismatch (%s-%s)" % (
458                                         version, build
459                                 )
460                         )
461                         return False
462
463         def _save_to_cache(self):
464                 _moduleLogger.info("Saving cache")
465                 if self._cachePath is None:
466                         return
467                 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
468
469                 try:
470                         dataToDump = (
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
476                         )
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")
482
483         def _clear_cache(self):
484                 updateContacts = len(self._contacts) != 0
485                 updateMessages = len(self._messages) != 0
486                 updateHistory = len(self._history) != 0
487                 oldDnd = self._dnd
488                 oldCallback = self._callback
489
490                 self._contacts = {}
491                 self._contactUpdateTime = datetime.datetime(1, 1, 1)
492                 self._messages = []
493                 self._messageUpdateTime = datetime.datetime(1, 1, 1)
494                 self._history = []
495                 self._historyUpdateTime = datetime.datetime(1, 1, 1)
496                 self._dnd = False
497                 self._callback = ""
498
499                 if updateContacts:
500                         self.contactsUpdated.emit()
501                 if updateMessages:
502                         self.messagesUpdated.emit()
503                 if updateHistory:
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)
509
510                 self._save_to_cache()
511
512         def _update_contacts(self):
513                 try:
514                         self._contacts = yield (
515                                 self._backend[0].get_contacts,
516                                 (),
517                                 {},
518                         )
519                 except Exception, e:
520                         self.error.emit(str(e))
521                         return
522                 self._contactUpdateTime = datetime.datetime.now()
523                 self.contactsUpdated.emit()
524
525         def _update_messages(self):
526                 try:
527                         self._messages = yield (
528                                 self._backend[0].get_messages,
529                                 (),
530                                 {},
531                         )
532                 except Exception, e:
533                         self.error.emit(str(e))
534                         return
535                 self._messageUpdateTime = datetime.datetime.now()
536                 self.messagesUpdated.emit()
537
538         def _update_history(self):
539                 try:
540                         self._history = yield (
541                                 self._backend[0].get_recent,
542                                 (),
543                                 {},
544                         )
545                 except Exception, e:
546                         self.error.emit(str(e))
547                         return
548                 self._historyUpdateTime = datetime.datetime.now()
549                 self.historyUpdated.emit()
550
551         def _update_dnd(self):
552                 oldDnd = self._dnd
553                 try:
554                         self._dnd = yield (
555                                 self._backend[0].is_dnd,
556                                 (),
557                                 {},
558                         )
559                 except Exception, e:
560                         self.error.emit(str(e))
561                         return
562                 if oldDnd != self._dnd:
563                         self.dndStateChange(self._dnd)
564
565         def _perform_op_while_loggedin(self, op):
566                 if self.state == self.LOGGEDIN_STATE:
567                         op.start()
568                 else:
569                         self._push_login_op(op)
570
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)
575                         return
576                 self._loginOps.append(asyncOp)