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