Fixing non-cannonical numbers
[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 = [misc_utils.make_ugly(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         # @todo Somehow add support for csv contacts
156
157         stateChange = QtCore.pyqtSignal(str)
158         loggedOut = QtCore.pyqtSignal()
159         loggedIn = QtCore.pyqtSignal()
160         callbackNumberChanged = QtCore.pyqtSignal(str)
161
162         contactsUpdated = QtCore.pyqtSignal()
163         messagesUpdated = QtCore.pyqtSignal()
164         historyUpdated = QtCore.pyqtSignal()
165         dndStateChange = QtCore.pyqtSignal(bool)
166
167         error = QtCore.pyqtSignal(str)
168
169         LOGGEDOUT_STATE = "logged out"
170         LOGGINGIN_STATE = "logging in"
171         LOGGEDIN_STATE = "logged in"
172
173         _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.1.90")
174
175         _LOGGEDOUT_TIME = -1
176         _LOGGINGIN_TIME = 0
177
178         def __init__(self, cachePath = None):
179                 QtCore.QObject.__init__(self)
180                 self._pool = qore_utils.AsyncPool()
181                 self._backend = []
182                 self._loggedInTime = self._LOGGEDOUT_TIME
183                 self._loginOps = []
184                 self._cachePath = cachePath
185                 self._username = None
186                 self._draft = Draft(self._pool, self._backend)
187
188                 self._contacts = {}
189                 self._contactUpdateTime = datetime.datetime(1971, 1, 1)
190                 self._messages = []
191                 self._messageUpdateTime = datetime.datetime(1971, 1, 1)
192                 self._history = []
193                 self._historyUpdateTime = datetime.datetime(1971, 1, 1)
194                 self._dnd = False
195                 self._callback = ""
196
197         @property
198         def state(self):
199                 return {
200                         self._LOGGEDOUT_TIME: self.LOGGEDOUT_STATE,
201                         self._LOGGINGIN_TIME: self.LOGGINGIN_STATE,
202                 }.get(self._loggedInTime, self.LOGGEDIN_STATE)
203
204         @property
205         def draft(self):
206                 return self._draft
207
208         def login(self, username, password):
209                 assert self.state == self.LOGGEDOUT_STATE
210                 assert username != ""
211                 if self._cachePath is not None:
212                         cookiePath = os.path.join(self._cachePath, "%s.cookies" % username)
213                 else:
214                         cookiePath = None
215
216                 if self._username != username or not self._backend:
217                         from backends import gv_backend
218                         del self._backend[:]
219                         self._backend[0:0] = [gv_backend.GVDialer(cookiePath)]
220
221                 self._pool.start()
222                 le = concurrent.AsyncLinearExecution(self._pool, self._login)
223                 le.start(username, password)
224
225         def logout(self):
226                 assert self.state != self.LOGGEDOUT_STATE
227                 self._pool.stop()
228                 self._loggedInTime = self._LOGGEDOUT_TIME
229                 self._backend[0].persist()
230                 self._save_to_cache()
231
232         def clear(self):
233                 assert self.state == self.LOGGEDOUT_STATE
234                 self._backend[0].logout()
235                 del self._backend[0]
236                 self._clear_cache()
237                 self._draft.clear()
238
239         def logout_and_clear(self):
240                 assert self.state != self.LOGGEDOUT_STATE
241                 self._pool.stop()
242                 self._loggedInTime = self._LOGGEDOUT_TIME
243                 self.clear()
244
245         def update_contacts(self, force = True):
246                 if not force and self._contacts:
247                         return
248                 le = concurrent.AsyncLinearExecution(self._pool, self._update_contacts)
249                 self._perform_op_while_loggedin(le)
250
251         def get_contacts(self):
252                 return self._contacts
253
254         def get_when_contacts_updated(self):
255                 return self._contactUpdateTime
256
257         def update_messages(self, force = True):
258                 if not force and self._messages:
259                         return
260                 le = concurrent.AsyncLinearExecution(self._pool, self._update_messages)
261                 self._perform_op_while_loggedin(le)
262
263         def get_messages(self):
264                 return self._messages
265
266         def get_when_messages_updated(self):
267                 return self._messageUpdateTime
268
269         def update_history(self, force = True):
270                 if not force and self._history:
271                         return
272                 le = concurrent.AsyncLinearExecution(self._pool, self._update_history)
273                 self._perform_op_while_loggedin(le)
274
275         def get_history(self):
276                 return self._history
277
278         def get_when_history_updated(self):
279                 return self._historyUpdateTime
280
281         def update_dnd(self):
282                 le = concurrent.AsyncLinearExecution(self._pool, self._update_dnd)
283                 self._perform_op_while_loggedin(le)
284
285         def set_dnd(self, dnd):
286                 le = concurrent.AsyncLinearExecution(self._pool, self._set_dnd)
287                 le.start(dnd)
288
289         def _set_dnd(self, dnd):
290                 # I'm paranoid about our state geting out of sync so we set no matter
291                 # what but act as if we have the cannonical state
292                 assert self.state == self.LOGGEDIN_STATE
293                 oldDnd = self._dnd
294                 try:
295                         yield (
296                                 self._backend[0].set_dnd,
297                                 (dnd, ),
298                                 {},
299                         )
300                 except Exception, e:
301                         self.error.emit(str(e))
302                         return
303                 self._dnd = dnd
304                 if oldDnd != self._dnd:
305                         self.dndStateChange.emit(self._dnd)
306
307         def get_dnd(self):
308                 return self._dnd
309
310         def get_account_number(self):
311                 return self._backend[0].get_account_number()
312
313         def get_callback_numbers(self):
314                 # @todo Remove evilness (might call is_authed which can block)
315                 return self._backend[0].get_callback_numbers()
316
317         def get_callback_number(self):
318                 return self._callback
319
320         def set_callback_number(self, callback):
321                 le = concurrent.AsyncLinearExecution(self._pool, self._set_callback_number)
322                 le.start(callback)
323
324         def _set_callback_number(self, callback):
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)