Couldn't update the message in the SMS window, fixed
[gc-dialer] / src / session.py
1 from __future__ import with_statement
2
3 import os
4 import time
5 import datetime
6 import contextlib
7 import logging
8
9 try:
10         import cPickle
11         pickle = cPickle
12 except ImportError:
13         import pickle
14
15 from PyQt4 import QtCore
16
17 from util import qore_utils
18 from util import qui_utils
19 from util import concurrent
20 from util import misc as misc_utils
21
22 import constants
23
24
25 _moduleLogger = logging.getLogger(__name__)
26
27
28 class _DraftContact(object):
29
30         def __init__(self, title, description, numbersWithDescriptions):
31                 self.title = title
32                 self.description = description
33                 self.numbers = numbersWithDescriptions
34                 self.selectedNumber = numbersWithDescriptions[0][0]
35
36
37 class Draft(QtCore.QObject):
38
39         sendingMessage = QtCore.pyqtSignal()
40         sentMessage = QtCore.pyqtSignal()
41         calling = QtCore.pyqtSignal()
42         called = QtCore.pyqtSignal()
43         cancelling = QtCore.pyqtSignal()
44         cancelled = QtCore.pyqtSignal()
45         error = QtCore.pyqtSignal(str)
46
47         recipientsChanged = QtCore.pyqtSignal()
48
49         def __init__(self, pool, backend, errorLog):
50                 QtCore.QObject.__init__(self)
51                 self._errorLog = errorLog
52                 self._contacts = {}
53                 self._pool = pool
54                 self._backend = backend
55                 self._busyReason = None
56                 self._message = ""
57
58         def send(self):
59                 assert 0 < len(self._contacts), "No contacts selected"
60                 assert 0 < len(self._message), "No message to send"
61                 numbers = [misc_utils.make_ugly(contact.selectedNumber) for contact in self._contacts.itervalues()]
62                 le = concurrent.AsyncLinearExecution(self._pool, self._send)
63                 le.start(numbers, self._message)
64
65         def call(self):
66                 assert len(self._contacts) == 1, "Must select 1 and only 1 contact"
67                 assert len(self._message) == 0, "Cannot send message with call"
68                 (contact, ) = self._contacts.itervalues()
69                 number = misc_utils.make_ugly(contact.selectedNumber)
70                 le = concurrent.AsyncLinearExecution(self._pool, self._call)
71                 le.start(number)
72
73         def cancel(self):
74                 le = concurrent.AsyncLinearExecution(self._pool, self._cancel)
75                 le.start()
76
77         def _get_message(self):
78                 return self._message
79
80         def _set_message(self, message):
81                 self._message = message
82
83         message = property(_get_message, _set_message)
84
85         def add_contact(self, contactId, title, description, numbersWithDescriptions):
86                 if self._busyReason is not None:
87                         raise RuntimeError("Please wait for %r" % self._busyReason)
88                 # Allow overwriting of contacts so that the message can be updated and the SMS dialog popped back up
89                 contactDetails = _DraftContact(title, description, numbersWithDescriptions)
90                 self._contacts[contactId] = contactDetails
91                 self.recipientsChanged.emit()
92
93         def remove_contact(self, contactId):
94                 if self._busyReason is not None:
95                         raise RuntimeError("Please wait for %r" % self._busyReason)
96                 assert contactId in self._contacts, "Contact missing"
97                 del self._contacts[contactId]
98                 self.recipientsChanged.emit()
99
100         def get_contacts(self):
101                 return self._contacts.iterkeys()
102
103         def get_num_contacts(self):
104                 return len(self._contacts)
105
106         def get_title(self, cid):
107                 return self._contacts[cid].title
108
109         def get_description(self, cid):
110                 return self._contacts[cid].description
111
112         def get_numbers(self, cid):
113                 return self._contacts[cid].numbers
114
115         def get_selected_number(self, cid):
116                 return self._contacts[cid].selectedNumber
117
118         def set_selected_number(self, cid, number):
119                 # @note I'm lazy, this isn't firing any kind of signal since only one
120                 # controller right now and that is the viewer
121                 assert number in (nWD[0] for nWD in self._contacts[cid].numbers), "Number not selectable"
122                 self._contacts[cid].selectedNumber = number
123
124         def clear(self):
125                 if self._busyReason is not None:
126                         raise RuntimeError("Please wait for %r" % self._busyReason)
127                 self._clear()
128
129         def _clear(self):
130                 oldContacts = self._contacts
131                 self._contacts = {}
132                 self._message = ""
133                 if oldContacts:
134                         self.recipientsChanged.emit()
135
136         @contextlib.contextmanager
137         def _busy(self, message):
138                 if self._busyReason is not None:
139                         raise RuntimeError("Already busy doing %r" % self._busyReason)
140                 try:
141                         self._busyReason = message
142                         yield
143                 finally:
144                         self._busyReason = None
145
146         def _send(self, numbers, text):
147                 self.sendingMessage.emit()
148                 try:
149                         with self._busy("Sending Text"):
150                                 with qui_utils.notify_busy(self._errorLog, "Sending Text"):
151                                         yield (
152                                                 self._backend[0].send_sms,
153                                                 (numbers, text),
154                                                 {},
155                                         )
156                                 self.sentMessage.emit()
157                                 self._clear()
158                 except Exception, e:
159                         _moduleLogger.exception("Reporting error to user")
160                         self.error.emit(str(e))
161
162         def _call(self, number):
163                 self.calling.emit()
164                 try:
165                         with self._busy("Calling"):
166                                 with qui_utils.notify_busy(self._errorLog, "Calling"):
167                                         yield (
168                                                 self._backend[0].call,
169                                                 (number, ),
170                                                 {},
171                                         )
172                                 self.called.emit()
173                                 self._clear()
174                 except Exception, e:
175                         _moduleLogger.exception("Reporting error to user")
176                         self.error.emit(str(e))
177
178         def _cancel(self):
179                 self.cancelling.emit()
180                 try:
181                         with qui_utils.notify_busy(self._errorLog, "Cancelling"):
182                                 yield (
183                                         self._backend[0].cancel,
184                                         (),
185                                         {},
186                                 )
187                         self.cancelled.emit()
188                 except Exception, e:
189                         _moduleLogger.exception("Reporting error to user")
190                         self.error.emit(str(e))
191
192
193 class Session(QtCore.QObject):
194
195         # @todo Somehow add support for csv contacts
196
197         stateChange = QtCore.pyqtSignal(str)
198         loggedOut = QtCore.pyqtSignal()
199         loggedIn = QtCore.pyqtSignal()
200         callbackNumberChanged = QtCore.pyqtSignal(str)
201
202         contactsUpdated = QtCore.pyqtSignal()
203         messagesUpdated = QtCore.pyqtSignal()
204         historyUpdated = QtCore.pyqtSignal()
205         dndStateChange = QtCore.pyqtSignal(bool)
206
207         error = QtCore.pyqtSignal(str)
208
209         LOGGEDOUT_STATE = "logged out"
210         LOGGINGIN_STATE = "logging in"
211         LOGGEDIN_STATE = "logged in"
212
213         _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.1.90")
214
215         _LOGGEDOUT_TIME = -1
216         _LOGGINGIN_TIME = 0
217
218         def __init__(self, errorLog, cachePath = None):
219                 QtCore.QObject.__init__(self)
220                 self._errorLog = errorLog
221                 self._pool = qore_utils.AsyncPool()
222                 self._backend = []
223                 self._loggedInTime = self._LOGGEDOUT_TIME
224                 self._loginOps = []
225                 self._cachePath = cachePath
226                 self._username = None
227                 self._draft = Draft(self._pool, self._backend, self._errorLog)
228
229                 self._contacts = {}
230                 self._contactUpdateTime = datetime.datetime(1971, 1, 1)
231                 self._messages = []
232                 self._messageUpdateTime = datetime.datetime(1971, 1, 1)
233                 self._history = []
234                 self._historyUpdateTime = datetime.datetime(1971, 1, 1)
235                 self._dnd = False
236                 self._callback = ""
237
238         @property
239         def state(self):
240                 return {
241                         self._LOGGEDOUT_TIME: self.LOGGEDOUT_STATE,
242                         self._LOGGINGIN_TIME: self.LOGGINGIN_STATE,
243                 }.get(self._loggedInTime, self.LOGGEDIN_STATE)
244
245         @property
246         def draft(self):
247                 return self._draft
248
249         def login(self, username, password):
250                 assert self.state == self.LOGGEDOUT_STATE, "Can only log-in when logged out (currently %s" % self.state
251                 assert username != "", "No username specified"
252                 if self._cachePath is not None:
253                         cookiePath = os.path.join(self._cachePath, "%s.cookies" % username)
254                 else:
255                         cookiePath = None
256
257                 if self._username != username or not self._backend:
258                         from backends import gv_backend
259                         del self._backend[:]
260                         self._backend[0:0] = [gv_backend.GVDialer(cookiePath)]
261
262                 self._pool.start()
263                 le = concurrent.AsyncLinearExecution(self._pool, self._login)
264                 le.start(username, password)
265
266         def logout(self):
267                 assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state
268                 _moduleLogger.info("Logging out")
269                 self._pool.stop()
270                 self._loggedInTime = self._LOGGEDOUT_TIME
271                 self._backend[0].persist()
272                 self._save_to_cache()
273                 self.stateChange.emit(self.LOGGEDOUT_STATE)
274                 self.loggedOut.emit()
275
276         def clear(self):
277                 assert self.state == self.LOGGEDOUT_STATE, "Can only clear when logged out (currently %s" % self.state
278                 self._backend[0].logout()
279                 del self._backend[0]
280                 self._clear_cache()
281                 self._draft.clear()
282
283         def logout_and_clear(self):
284                 assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state
285                 _moduleLogger.info("Logging out and clearing the account")
286                 self._pool.stop()
287                 self._loggedInTime = self._LOGGEDOUT_TIME
288                 self.clear()
289                 self.stateChange.emit(self.LOGGEDOUT_STATE)
290                 self.loggedOut.emit()
291
292         def update_contacts(self, force = True):
293                 if not force and self._contacts:
294                         return
295                 le = concurrent.AsyncLinearExecution(self._pool, self._update_contacts)
296                 self._perform_op_while_loggedin(le)
297
298         def get_contacts(self):
299                 return self._contacts
300
301         def get_when_contacts_updated(self):
302                 return self._contactUpdateTime
303
304         def update_messages(self, force = True):
305                 if not force and self._messages:
306                         return
307                 le = concurrent.AsyncLinearExecution(self._pool, self._update_messages)
308                 self._perform_op_while_loggedin(le)
309
310         def get_messages(self):
311                 return self._messages
312
313         def get_when_messages_updated(self):
314                 return self._messageUpdateTime
315
316         def update_history(self, force = True):
317                 if not force and self._history:
318                         return
319                 le = concurrent.AsyncLinearExecution(self._pool, self._update_history)
320                 self._perform_op_while_loggedin(le)
321
322         def get_history(self):
323                 return self._history
324
325         def get_when_history_updated(self):
326                 return self._historyUpdateTime
327
328         def update_dnd(self):
329                 le = concurrent.AsyncLinearExecution(self._pool, self._update_dnd)
330                 self._perform_op_while_loggedin(le)
331
332         def set_dnd(self, dnd):
333                 le = concurrent.AsyncLinearExecution(self._pool, self._set_dnd)
334                 le.start(dnd)
335
336         def _set_dnd(self, dnd):
337                 oldDnd = self._dnd
338                 try:
339                         assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state
340                         with qui_utils.notify_busy(self._errorLog, "Setting DND Status"):
341                                 yield (
342                                         self._backend[0].set_dnd,
343                                         (dnd, ),
344                                         {},
345                                 )
346                 except Exception, e:
347                         _moduleLogger.exception("Reporting error to user")
348                         self.error.emit(str(e))
349                         return
350                 self._dnd = dnd
351                 if oldDnd != self._dnd:
352                         self.dndStateChange.emit(self._dnd)
353
354         def get_dnd(self):
355                 return self._dnd
356
357         def get_account_number(self):
358                 if self.state != self.LOGGEDIN_STATE:
359                         return ""
360                 return self._backend[0].get_account_number()
361
362         def get_callback_numbers(self):
363                 if self.state != self.LOGGEDIN_STATE:
364                         return {}
365                 return self._backend[0].get_callback_numbers()
366
367         def get_callback_number(self):
368                 return self._callback
369
370         def set_callback_number(self, callback):
371                 le = concurrent.AsyncLinearExecution(self._pool, self._set_callback_number)
372                 le.start(callback)
373
374         def _set_callback_number(self, callback):
375                 oldCallback = self._callback
376                 try:
377                         assert self.state == self.LOGGEDIN_STATE, "Callbacks configurable only when logged in (currently %s" % self.state
378                         yield (
379                                 self._backend[0].set_callback_number,
380                                 (callback, ),
381                                 {},
382                         )
383                 except Exception, e:
384                         _moduleLogger.exception("Reporting error to user")
385                         self.error.emit(str(e))
386                         return
387                 self._callback = callback
388                 if oldCallback != self._callback:
389                         self.callbackNumberChanged.emit(self._callback)
390
391         def _login(self, username, password):
392                 with qui_utils.notify_busy(self._errorLog, "Logging In"):
393                         self._loggedInTime = self._LOGGINGIN_TIME
394                         self.stateChange.emit(self.LOGGINGIN_STATE)
395                         finalState = self.LOGGEDOUT_STATE
396                         isLoggedIn = False
397                         try:
398                                 if not isLoggedIn and self._backend[0].is_quick_login_possible():
399                                         isLoggedIn = yield (
400                                                 self._backend[0].is_authed,
401                                                 (),
402                                                 {},
403                                         )
404                                         if isLoggedIn:
405                                                 _moduleLogger.info("Logged in through cookies")
406                                         else:
407                                                 # Force a clearing of the cookies
408                                                 yield (
409                                                         self._backend[0].logout,
410                                                         (),
411                                                         {},
412                                                 )
413
414                                 if not isLoggedIn:
415                                         isLoggedIn = yield (
416                                                 self._backend[0].login,
417                                                 (username, password),
418                                                 {},
419                                         )
420                                         if isLoggedIn:
421                                                 _moduleLogger.info("Logged in through credentials")
422
423                                 if isLoggedIn:
424                                         self._loggedInTime = int(time.time())
425                                         oldUsername = self._username
426                                         self._username = username
427                                         finalState = self.LOGGEDIN_STATE
428                                         if oldUsername != self._username:
429                                                 needOps = not self._load()
430                                         else:
431                                                 needOps = True
432
433                                         self.loggedIn.emit()
434                                         self.stateChange.emit(finalState)
435                                         finalState = None # Mark it as already set
436
437                                         if needOps:
438                                                 loginOps = self._loginOps[:]
439                                         else:
440                                                 loginOps = []
441                                         del self._loginOps[:]
442                                         for asyncOp in loginOps:
443                                                 asyncOp.start()
444                                 else:
445                                         self._loggedInTime = self._LOGGEDOUT_TIME
446                                         self.error.emit("Error logging in")
447                         except Exception, e:
448                                 self._loggedInTime = self._LOGGEDOUT_TIME
449                                 _moduleLogger.exception("Reporting error to user")
450                                 self.error.emit(str(e))
451                         finally:
452                                 if finalState is not None:
453                                         self.stateChange.emit(finalState)
454                         if isLoggedIn and self._callback:
455                                 self.set_callback_number(self._callback)
456
457         def _load(self):
458                 updateContacts = len(self._contacts) != 0
459                 updateMessages = len(self._messages) != 0
460                 updateHistory = len(self._history) != 0
461                 oldDnd = self._dnd
462                 oldCallback = self._callback
463
464                 self._contacts = {}
465                 self._messages = []
466                 self._history = []
467                 self._dnd = False
468                 self._callback = ""
469
470                 loadedFromCache = self._load_from_cache()
471                 if loadedFromCache:
472                         updateContacts = True
473                         updateMessages = True
474                         updateHistory = True
475
476                 if updateContacts:
477                         self.contactsUpdated.emit()
478                 if updateMessages:
479                         self.messagesUpdated.emit()
480                 if updateHistory:
481                         self.historyUpdated.emit()
482                 if oldDnd != self._dnd:
483                         self.dndStateChange.emit(self._dnd)
484                 if oldCallback != self._callback:
485                         self.callbackNumberChanged.emit(self._callback)
486
487                 return loadedFromCache
488
489         def _load_from_cache(self):
490                 if self._cachePath is None:
491                         return False
492                 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
493
494                 try:
495                         with open(cachePath, "rb") as f:
496                                 dumpedData = pickle.load(f)
497                 except (pickle.PickleError, IOError, EOFError, ValueError):
498                         _moduleLogger.exception("Pickle fun loading")
499                         return False
500                 except:
501                         _moduleLogger.exception("Weirdness loading")
502                         return False
503
504                 try:
505                         (
506                                 version, build,
507                                 contacts, contactUpdateTime,
508                                 messages, messageUpdateTime,
509                                 history, historyUpdateTime,
510                                 dnd, callback
511                         ) = dumpedData
512                 except ValueError:
513                         _moduleLogger.exception("Upgrade/downgrade fun")
514                         return False
515                 except:
516                         _moduleLogger.exception("Weirdlings")
517
518                 if misc_utils.compare_versions(
519                         self._OLDEST_COMPATIBLE_FORMAT_VERSION,
520                         misc_utils.parse_version(version),
521                 ) <= 0:
522                         _moduleLogger.info("Loaded cache")
523                         self._contacts = contacts
524                         self._contactUpdateTime = contactUpdateTime
525                         self._messages = messages
526                         self._messageUpdateTime = messageUpdateTime
527                         self._history = history
528                         self._historyUpdateTime = historyUpdateTime
529                         self._dnd = dnd
530                         self._callback = callback
531                         return True
532                 else:
533                         _moduleLogger.debug(
534                                 "Skipping cache due to version mismatch (%s-%s)" % (
535                                         version, build
536                                 )
537                         )
538                         return False
539
540         def _save_to_cache(self):
541                 _moduleLogger.info("Saving cache")
542                 if self._cachePath is None:
543                         return
544                 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
545
546                 try:
547                         dataToDump = (
548                                 constants.__version__, constants.__build__,
549                                 self._contacts, self._contactUpdateTime,
550                                 self._messages, self._messageUpdateTime,
551                                 self._history, self._historyUpdateTime,
552                                 self._dnd, self._callback
553                         )
554                         with open(cachePath, "wb") as f:
555                                 pickle.dump(dataToDump, f, pickle.HIGHEST_PROTOCOL)
556                         _moduleLogger.info("Cache saved")
557                 except (pickle.PickleError, IOError):
558                         _moduleLogger.exception("While saving")
559
560         def _clear_cache(self):
561                 updateContacts = len(self._contacts) != 0
562                 updateMessages = len(self._messages) != 0
563                 updateHistory = len(self._history) != 0
564                 oldDnd = self._dnd
565                 oldCallback = self._callback
566
567                 self._contacts = {}
568                 self._contactUpdateTime = datetime.datetime(1971, 1, 1)
569                 self._messages = []
570                 self._messageUpdateTime = datetime.datetime(1971, 1, 1)
571                 self._history = []
572                 self._historyUpdateTime = datetime.datetime(1971, 1, 1)
573                 self._dnd = False
574                 self._callback = ""
575
576                 if updateContacts:
577                         self.contactsUpdated.emit()
578                 if updateMessages:
579                         self.messagesUpdated.emit()
580                 if updateHistory:
581                         self.historyUpdated.emit()
582                 if oldDnd != self._dnd:
583                         self.dndStateChange.emit(self._dnd)
584                 if oldCallback != self._callback:
585                         self.callbackNumberChanged.emit(self._callback)
586
587                 self._save_to_cache()
588
589         def _update_contacts(self):
590                 try:
591                         assert self.state == self.LOGGEDIN_STATE, "Contacts requires being logged in (currently %s" % self.state
592                         with qui_utils.notify_busy(self._errorLog, "Updating Contacts"):
593                                 self._contacts = yield (
594                                         self._backend[0].get_contacts,
595                                         (),
596                                         {},
597                                 )
598                 except Exception, e:
599                         _moduleLogger.exception("Reporting error to user")
600                         self.error.emit(str(e))
601                         return
602                 self._contactUpdateTime = datetime.datetime.now()
603                 self.contactsUpdated.emit()
604
605         def _update_messages(self):
606                 try:
607                         assert self.state == self.LOGGEDIN_STATE, "Messages requires being logged in (currently %s" % self.state
608                         with qui_utils.notify_busy(self._errorLog, "Updating Messages"):
609                                 self._messages = yield (
610                                         self._backend[0].get_messages,
611                                         (),
612                                         {},
613                                 )
614                 except Exception, e:
615                         _moduleLogger.exception("Reporting error to user")
616                         self.error.emit(str(e))
617                         return
618                 self._messageUpdateTime = datetime.datetime.now()
619                 self.messagesUpdated.emit()
620
621         def _update_history(self):
622                 try:
623                         assert self.state == self.LOGGEDIN_STATE, "History requires being logged in (currently %s" % self.state
624                         with qui_utils.notify_busy(self._errorLog, "Updating History"):
625                                 self._history = yield (
626                                         self._backend[0].get_recent,
627                                         (),
628                                         {},
629                                 )
630                 except Exception, e:
631                         _moduleLogger.exception("Reporting error to user")
632                         self.error.emit(str(e))
633                         return
634                 self._historyUpdateTime = datetime.datetime.now()
635                 self.historyUpdated.emit()
636
637         def _update_dnd(self):
638                 oldDnd = self._dnd
639                 try:
640                         assert self.state == self.LOGGEDIN_STATE, "DND requires being logged in (currently %s" % self.state
641                         self._dnd = yield (
642                                 self._backend[0].is_dnd,
643                                 (),
644                                 {},
645                         )
646                 except Exception, e:
647                         _moduleLogger.exception("Reporting error to user")
648                         self.error.emit(str(e))
649                         return
650                 if oldDnd != self._dnd:
651                         self.dndStateChange(self._dnd)
652
653         def _perform_op_while_loggedin(self, op):
654                 if self.state == self.LOGGEDIN_STATE:
655                         op.start()
656                 else:
657                         self._push_login_op(op)
658
659         def _push_login_op(self, asyncOp):
660                 assert self.state != self.LOGGEDIN_STATE, "Can only queue work when logged out"
661                 if asyncOp in self._loginOps:
662                         _moduleLogger.info("Skipping queueing duplicate op: %r" % asyncOp)
663                         return
664                 self._loginOps.append(asyncOp)