Fixing an issue with setting the callback
[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                 le = concurrent.AsyncLinearExecution(self._pool, self._set_dnd)
285                 le.start(dnd)
286
287         def _set_dnd(self, dnd):
288                 # @bug This should be exposed a different way
289                 # I'm paranoid about our state geting out of sync so we set no matter
290                 # what but act as if we have the cannonical state
291                 assert self.state == self.LOGGEDIN_STATE
292                 oldDnd = self._dnd
293                 try:
294                         yield (
295                                 self._backend[0].set_dnd,
296                                 (dnd, ),
297                                 {},
298                         )
299                 except Exception, e:
300                         self.error.emit(str(e))
301                         return
302                 self._dnd = dnd
303                 if oldDnd != self._dnd:
304                         self.dndStateChange.emit(self._dnd)
305
306         def get_dnd(self):
307                 return self._dnd
308
309         def get_account_number(self):
310                 return self._backend[0].get_account_number()
311
312         def get_callback_numbers(self):
313                 # @todo Remove evilness (might call is_authed which can block)
314                 return self._backend[0].get_callback_numbers()
315
316         def get_callback_number(self):
317                 return self._callback
318
319         def set_callback_number(self, callback):
320                 le = concurrent.AsyncLinearExecution(self._pool, self._set_callback_number)
321                 le.start(callback)
322
323         def _set_callback_number(self, callback):
324                 # @bug This should be exposed a different way
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
329                 try:
330                         yield (
331                                 self._backend[0].set_callback_number,
332                                 (callback, ),
333                                 {},
334                         )
335                 except Exception, e:
336                         self.error.emit(str(e))
337                         return
338                 self._callback = callback
339                 if oldCallback != self._callback:
340                         self.callbackNumberChanged.emit(self._callback)
341
342         def _login(self, username, password):
343                 self._loggedInTime = self._LOGGINGIN_TIME
344                 self.stateChange.emit(self.LOGGINGIN_STATE)
345                 finalState = self.LOGGEDOUT_STATE
346                 try:
347                         isLoggedIn = False
348
349                         if not isLoggedIn and self._backend[0].is_quick_login_possible():
350                                 isLoggedIn = yield (
351                                         self._backend[0].is_authed,
352                                         (),
353                                         {},
354                                 )
355                                 if isLoggedIn:
356                                         _moduleLogger.info("Logged in through cookies")
357                                 else:
358                                         # Force a clearing of the cookies
359                                         yield (
360                                                 self._backend[0].logout,
361                                                 (),
362                                                 {},
363                                         )
364
365                         if not isLoggedIn:
366                                 isLoggedIn = yield (
367                                         self._backend[0].login,
368                                         (username, password),
369                                         {},
370                                 )
371                                 if isLoggedIn:
372                                         _moduleLogger.info("Logged in through credentials")
373
374                         if isLoggedIn:
375                                 self._loggedInTime = int(time.time())
376                                 oldUsername = self._username
377                                 self._username = username
378                                 finalState = self.LOGGEDIN_STATE
379                                 self.loggedIn.emit()
380                                 if oldUsername != self._username:
381                                         needOps = not self._load()
382                                 else:
383                                         needOps = True
384                                 if needOps:
385                                         loginOps = self._loginOps[:]
386                                 else:
387                                         loginOps = []
388                                 del self._loginOps[:]
389                                 for asyncOp in loginOps:
390                                         asyncOp.start()
391                 except Exception, e:
392                         self.error.emit(str(e))
393                 finally:
394                         self.stateChange.emit(finalState)
395
396         def _load(self):
397                 updateContacts = len(self._contacts) != 0
398                 updateMessages = len(self._messages) != 0
399                 updateHistory = len(self._history) != 0
400                 oldDnd = self._dnd
401                 oldCallback = self._callback
402
403                 self._contacts = {}
404                 self._messages = []
405                 self._history = []
406                 self._dnd = False
407                 self._callback = ""
408
409                 loadedFromCache = self._load_from_cache()
410                 if loadedFromCache:
411                         updateContacts = True
412                         updateMessages = True
413                         updateHistory = True
414
415                 if updateContacts:
416                         self.contactsUpdated.emit()
417                 if updateMessages:
418                         self.messagesUpdated.emit()
419                 if updateHistory:
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)
425
426                 return loadedFromCache
427
428         def _load_from_cache(self):
429                 if self._cachePath is None:
430                         return False
431                 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
432
433                 try:
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")
438                         return False
439                 except:
440                         _moduleLogger.exception("Weirdness loading")
441                         return False
442
443                 (
444                         version, build,
445                         contacts, contactUpdateTime,
446                         messages, messageUpdateTime,
447                         history, historyUpdateTime,
448                         dnd, callback
449                 ) = dumpedData
450
451                 if misc_utils.compare_versions(
452                         self._OLDEST_COMPATIBLE_FORMAT_VERSION,
453                         misc_utils.parse_version(version),
454                 ) <= 0:
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
462                         self._dnd = dnd
463                         self._callback = callback
464                         return True
465                 else:
466                         _moduleLogger.debug(
467                                 "Skipping cache due to version mismatch (%s-%s)" % (
468                                         version, build
469                                 )
470                         )
471                         return False
472
473         def _save_to_cache(self):
474                 _moduleLogger.info("Saving cache")
475                 if self._cachePath is None:
476                         return
477                 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
478
479                 try:
480                         dataToDump = (
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
486                         )
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")
492
493         def _clear_cache(self):
494                 updateContacts = len(self._contacts) != 0
495                 updateMessages = len(self._messages) != 0
496                 updateHistory = len(self._history) != 0
497                 oldDnd = self._dnd
498                 oldCallback = self._callback
499
500                 self._contacts = {}
501                 self._contactUpdateTime = datetime.datetime(1, 1, 1)
502                 self._messages = []
503                 self._messageUpdateTime = datetime.datetime(1, 1, 1)
504                 self._history = []
505                 self._historyUpdateTime = datetime.datetime(1, 1, 1)
506                 self._dnd = False
507                 self._callback = ""
508
509                 if updateContacts:
510                         self.contactsUpdated.emit()
511                 if updateMessages:
512                         self.messagesUpdated.emit()
513                 if updateHistory:
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)
519
520                 self._save_to_cache()
521
522         def _update_contacts(self):
523                 try:
524                         self._contacts = yield (
525                                 self._backend[0].get_contacts,
526                                 (),
527                                 {},
528                         )
529                 except Exception, e:
530                         self.error.emit(str(e))
531                         return
532                 self._contactUpdateTime = datetime.datetime.now()
533                 self.contactsUpdated.emit()
534
535         def _update_messages(self):
536                 try:
537                         self._messages = yield (
538                                 self._backend[0].get_messages,
539                                 (),
540                                 {},
541                         )
542                 except Exception, e:
543                         self.error.emit(str(e))
544                         return
545                 self._messageUpdateTime = datetime.datetime.now()
546                 self.messagesUpdated.emit()
547
548         def _update_history(self):
549                 try:
550                         self._history = yield (
551                                 self._backend[0].get_recent,
552                                 (),
553                                 {},
554                         )
555                 except Exception, e:
556                         self.error.emit(str(e))
557                         return
558                 self._historyUpdateTime = datetime.datetime.now()
559                 self.historyUpdated.emit()
560
561         def _update_dnd(self):
562                 oldDnd = self._dnd
563                 try:
564                         self._dnd = yield (
565                                 self._backend[0].is_dnd,
566                                 (),
567                                 {},
568                         )
569                 except Exception, e:
570                         self.error.emit(str(e))
571                         return
572                 if oldDnd != self._dnd:
573                         self.dndStateChange(self._dnd)
574
575         def _perform_op_while_loggedin(self, op):
576                 if self.state == self.LOGGEDIN_STATE:
577                         op.start()
578                 else:
579                         self._push_login_op(op)
580
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)
585                         return
586                 self._loginOps.append(asyncOp)