Actually implementing send and call
[gc-dialer] / src / session.py
1 import os
2 import time
3 import logging
4
5 try:
6         import cPickle
7         pickle = cPickle
8 except ImportError:
9         import pickle
10
11 from PyQt4 import QtCore
12
13 from util import qore_utils
14 from util import concurrent
15 from util import misc as misc_utils
16
17 import constants
18
19
20 _moduleLogger = logging.getLogger(__name__)
21
22
23 class _DraftContact(object):
24
25         def __init__(self, title, description, numbersWithDescriptions):
26                 self.title = title
27                 self.description = description
28                 self.numbers = numbersWithDescriptions
29                 self.selectedNumber = numbersWithDescriptions[0][0]
30
31
32 class Draft(QtCore.QObject):
33
34         sendingMessage = QtCore.pyqtSignal()
35         sentMessage = QtCore.pyqtSignal()
36         calling = QtCore.pyqtSignal()
37         called = QtCore.pyqtSignal()
38         cancelling = QtCore.pyqtSignal()
39         cancelled = QtCore.pyqtSignal()
40         error = QtCore.pyqtSignal(str)
41
42         recipientsChanged = QtCore.pyqtSignal()
43
44         def __init__(self, pool, backend):
45                 QtCore.QObject.__init__(self)
46                 self._contacts = {}
47                 self._pool = pool
48                 self._backend = backend
49
50         def send(self, text):
51                 assert 0 < len(self._contacts)
52                 numbers = [contact.selectedNumber for contact in self._contacts.itervalues()]
53                 le = concurrent.AsyncLinearExecution(self._pool, self._send)
54                 le.start(numbers, text)
55
56         def call(self):
57                 assert len(self._contacts) == 1
58                 (contact, ) = self._contacts.itervalues()
59                 le = concurrent.AsyncLinearExecution(self._pool, self._call)
60                 le.start(contact.selectedNumber)
61
62         def cancel(self):
63                 le = concurrent.AsyncLinearExecution(self._pool, self._cancel)
64                 le.start()
65
66         def add_contact(self, contactId, title, description, numbersWithDescriptions):
67                 assert contactId not in self._contacts
68                 contactDetails = _DraftContact(title, description, numbersWithDescriptions)
69                 self._contacts[contactId] = contactDetails
70                 self.recipientsChanged.emit()
71
72         def remove_contact(self, contactId):
73                 assert contactId in self._contacts
74                 del self._contacts[contactId]
75                 self.recipientsChanged.emit()
76
77         def get_contacts(self):
78                 return self._contacts.iterkeys()
79
80         def get_num_contacts(self):
81                 return len(self._contacts)
82
83         def get_title(self, cid):
84                 return self._contacts[cid].title
85
86         def get_description(self, cid):
87                 return self._contacts[cid].description
88
89         def get_numbers(self, cid):
90                 return self._contacts[cid].numbers
91
92         def get_selected_number(self, cid):
93                 return self._contacts[cid].selectedNumber
94
95         def set_selected_number(self, cid, number):
96                 # @note I'm lazy, this isn't firing any kind of signal since only one
97                 # controller right now and that is the viewer
98                 assert number in (nWD[0] for nWD in self._contacts[cid].numbers)
99                 self._contacts[cid].selectedNumber = number
100
101         def clear(self):
102                 oldContacts = self._contacts
103                 self._contacts = {}
104                 if oldContacts:
105                         self.recipientsChanged.emit()
106
107         def _send(self, numbers, text):
108                 self.sendingMessage.emit()
109                 try:
110                         yield (
111                                 self._backend[0].send_sms,
112                                 (numbers, text),
113                                 {},
114                         )
115                         self.sentMessage.emit()
116                         self.clear()
117                 except Exception, e:
118                         self.error.emit(str(e))
119
120         def _call(self, number):
121                 self.calling.emit()
122                 try:
123                         yield (
124                                 self._backend[0].call,
125                                 (number, ),
126                                 {},
127                         )
128                         self.called.emit()
129                         self.clear()
130                 except Exception, e:
131                         self.error.emit(str(e))
132
133         def _cancel(self):
134                 self.cancelling.emit()
135                 try:
136                         yield (
137                                 self._backend[0].cancel,
138                                 (),
139                                 {},
140                         )
141                         self.cancelled.emit()
142                 except Exception, e:
143                         self.error.emit(str(e))
144
145
146 class Session(QtCore.QObject):
147
148         stateChange = QtCore.pyqtSignal(str)
149         loggedOut = QtCore.pyqtSignal()
150         loggedIn = QtCore.pyqtSignal()
151         callbackNumberChanged = QtCore.pyqtSignal(str)
152
153         contactsUpdated = QtCore.pyqtSignal()
154         messagesUpdated = QtCore.pyqtSignal()
155         historyUpdated = QtCore.pyqtSignal()
156         dndStateChange = QtCore.pyqtSignal(bool)
157
158         error = QtCore.pyqtSignal(str)
159
160         LOGGEDOUT_STATE = "logged out"
161         LOGGINGIN_STATE = "logging in"
162         LOGGEDIN_STATE = "logged in"
163
164         _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.2.0")
165
166         _LOGGEDOUT_TIME = -1
167         _LOGGINGIN_TIME = 0
168
169         def __init__(self, cachePath = None):
170                 QtCore.QObject.__init__(self)
171                 self._pool = qore_utils.AsyncPool()
172                 self._backend = []
173                 self._loggedInTime = self._LOGGEDOUT_TIME
174                 self._loginOps = []
175                 self._cachePath = cachePath
176                 self._username = None
177                 self._draft = Draft(self._pool, self._backend)
178
179                 self._contacts = {}
180                 self._messages = []
181                 self._history = []
182                 self._dnd = False
183                 self._callback = ""
184
185         @property
186         def state(self):
187                 return {
188                         self._LOGGEDOUT_TIME: self.LOGGEDOUT_STATE,
189                         self._LOGGINGIN_TIME: self.LOGGINGIN_STATE,
190                 }.get(self._loggedInTime, self.LOGGEDIN_STATE)
191
192         @property
193         def draft(self):
194                 return self._draft
195
196         def login(self, username, password):
197                 assert self.state == self.LOGGEDOUT_STATE
198                 assert username != ""
199                 if self._cachePath is not None:
200                         cookiePath = os.path.join(self._cachePath, "%s.cookies" % username)
201                 else:
202                         cookiePath = None
203
204                 if self._username != username or not self._backend:
205                         from backends import gv_backend
206                         del self._backend[:]
207                         self._backend[0:0] = [gv_backend.GVDialer(cookiePath)]
208
209                 self._pool.start()
210                 le = concurrent.AsyncLinearExecution(self._pool, self._login)
211                 le.start(username, password)
212
213         def logout(self):
214                 assert self.state != self.LOGGEDOUT_STATE
215                 self._pool.stop()
216                 self._loggedInTime = self._LOGGEDOUT_TIME
217                 self._backend[0].persist()
218                 self._save_to_cache()
219
220         def clear(self):
221                 assert self.state == self.LOGGEDOUT_STATE
222                 self._backend[0].logout()
223                 del self._backend[0]
224                 self._clear_cache()
225                 self._draft.clear()
226
227         def logout_and_clear(self):
228                 assert self.state != self.LOGGEDOUT_STATE
229                 self._pool.stop()
230                 self._loggedInTime = self._LOGGEDOUT_TIME
231                 self.clear()
232
233         def update_contacts(self, force = True):
234                 if not force and self._contacts:
235                         return
236                 le = concurrent.AsyncLinearExecution(self._pool, self._update_contacts)
237                 self._perform_op_while_loggedin(le)
238
239         def get_contacts(self):
240                 return self._contacts
241
242         def update_messages(self, force = True):
243                 if not force and self._messages:
244                         return
245                 le = concurrent.AsyncLinearExecution(self._pool, self._update_messages)
246                 self._perform_op_while_loggedin(le)
247
248         def get_messages(self):
249                 return self._messages
250
251         def update_history(self, force = True):
252                 if not force and self._history:
253                         return
254                 le = concurrent.AsyncLinearExecution(self._pool, self._update_history)
255                 self._perform_op_while_loggedin(le)
256
257         def get_history(self):
258                 return self._history
259
260         def update_dnd(self):
261                 le = concurrent.AsyncLinearExecution(self._pool, self._update_dnd)
262                 self._perform_op_while_loggedin(le)
263
264         def set_dnd(self, dnd):
265                 # I'm paranoid about our state geting out of sync so we set no matter
266                 # what but act as if we have the cannonical state
267                 assert self.state == self.LOGGEDIN_STATE
268                 oldDnd = self._dnd
269                 try:
270                         yield (
271                                 self._backend[0].set_dnd,
272                                 (dnd),
273                                 {},
274                         )
275                 except Exception, e:
276                         self.error.emit(str(e))
277                         return
278                 self._dnd = dnd
279                 if oldDnd != self._dnd:
280                         self.dndStateChange.emit(self._dnd)
281
282         def get_dnd(self):
283                 return self._dnd
284
285         def get_account_number(self):
286                 return self._backend[0].get_account_number()
287
288         def get_callback_numbers(self):
289                 # @todo Remove evilness
290                 return self._backend[0].get_callback_numbers()
291
292         def get_callback_number(self):
293                 return self._callback
294
295         def set_callback_number(self, callback):
296                 # I'm paranoid about our state geting out of sync so we set no matter
297                 # what but act as if we have the cannonical state
298                 assert self.state == self.LOGGEDIN_STATE
299                 oldCallback = self._callback
300                 try:
301                         yield (
302                                 self._backend[0].set_callback_number,
303                                 (callback),
304                                 {},
305                         )
306                 except Exception, e:
307                         self.error.emit(str(e))
308                         return
309                 self._callback = callback
310                 if oldCallback != self._callback:
311                         self.callbackNumberChanged.emit(self._callback)
312
313         def _login(self, username, password):
314                 self._loggedInTime = self._LOGGINGIN_TIME
315                 self.stateChange.emit(self.LOGGINGIN_STATE)
316                 finalState = self.LOGGEDOUT_STATE
317                 try:
318                         isLoggedIn = False
319
320                         if not isLoggedIn and self._backend[0].is_quick_login_possible():
321                                 isLoggedIn = yield (
322                                         self._backend[0].is_authed,
323                                         (),
324                                         {},
325                                 )
326                                 if isLoggedIn:
327                                         _moduleLogger.info("Logged in through cookies")
328                                 else:
329                                         # Force a clearing of the cookies
330                                         yield (
331                                                 self._backend[0].logout,
332                                                 (),
333                                                 {},
334                                         )
335
336                         if not isLoggedIn:
337                                 isLoggedIn = yield (
338                                         self._backend[0].login,
339                                         (username, password),
340                                         {},
341                                 )
342                                 if isLoggedIn:
343                                         _moduleLogger.info("Logged in through credentials")
344
345                         if isLoggedIn:
346                                 self._loggedInTime = int(time.time())
347                                 oldUsername = self._username
348                                 self._username = username
349                                 finalState = self.LOGGEDIN_STATE
350                                 self.loggedIn.emit()
351                                 if oldUsername != self._username:
352                                         needOps = not self._load()
353                                 else:
354                                         needOps = True
355                                 if needOps:
356                                         loginOps = self._loginOps[:]
357                                 else:
358                                         loginOps = []
359                                 del self._loginOps[:]
360                                 for asyncOp in loginOps:
361                                         asyncOp.start()
362                 except Exception, e:
363                         self.error.emit(str(e))
364                 finally:
365                         self.stateChange.emit(finalState)
366
367         def _load(self):
368                 updateContacts = len(self._contacts) != 0
369                 updateMessages = len(self._messages) != 0
370                 updateHistory = len(self._history) != 0
371                 oldDnd = self._dnd
372                 oldCallback = self._callback
373
374                 self._contacts = {}
375                 self._messages = []
376                 self._history = []
377                 self._dnd = False
378                 self._callback = ""
379
380                 loadedFromCache = self._load_from_cache()
381                 if loadedFromCache:
382                         updateContacts = True
383                         updateMessages = True
384                         updateHistory = True
385
386                 if updateContacts:
387                         self.contactsUpdated.emit()
388                 if updateMessages:
389                         self.messagesUpdated.emit()
390                 if updateHistory:
391                         self.historyUpdated.emit()
392                 if oldDnd != self._dnd:
393                         self.dndStateChange.emit(self._dnd)
394                 if oldCallback != self._callback:
395                         self.callbackNumberChanged.emit(self._callback)
396
397                 return loadedFromCache
398
399         def _load_from_cache(self):
400                 if self._cachePath is None:
401                         return False
402                 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
403
404                 try:
405                         with open(cachePath, "rb") as f:
406                                 dumpedData = pickle.load(f)
407                 except (pickle.PickleError, IOError, EOFError, ValueError):
408                         _moduleLogger.exception("Pickle fun loading")
409                         return False
410                 except:
411                         _moduleLogger.exception("Weirdness loading")
412                         return False
413
414                 (
415                         version, build,
416                         contacts, messages, history, dnd, callback
417                 ) = dumpedData
418
419                 if misc_utils.compare_versions(
420                         self._OLDEST_COMPATIBLE_FORMAT_VERSION,
421                         misc_utils.parse_version(version),
422                 ) <= 0:
423                         _moduleLogger.info("Loaded cache")
424                         self._contacts = contacts
425                         self._messages = messages
426                         self._history = history
427                         self._dnd = dnd
428                         self._callback = callback
429                         return True
430                 else:
431                         _moduleLogger.debug(
432                                 "Skipping cache due to version mismatch (%s-%s)" % (
433                                         version, build
434                                 )
435                         )
436                         return False
437
438         def _save_to_cache(self):
439                 _moduleLogger.info("Saving cache")
440                 if self._cachePath is None:
441                         return
442                 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
443
444                 try:
445                         dataToDump = (
446                                 constants.__version__, constants.__build__,
447                                 self._contacts, self._messages, self._history, self._dnd, self._callback
448                         )
449                         with open(cachePath, "wb") as f:
450                                 pickle.dump(dataToDump, f, pickle.HIGHEST_PROTOCOL)
451                         _moduleLogger.info("Cache saved")
452                 except (pickle.PickleError, IOError):
453                         _moduleLogger.exception("While saving")
454
455         def _clear_cache(self):
456                 updateContacts = len(self._contacts) != 0
457                 updateMessages = len(self._messages) != 0
458                 updateHistory = len(self._history) != 0
459                 oldDnd = self._dnd
460                 oldCallback = self._callback
461
462                 self._contacts = {}
463                 self._messages = []
464                 self._history = []
465                 self._dnd = False
466                 self._callback = ""
467
468                 if updateContacts:
469                         self.contactsUpdated.emit()
470                 if updateMessages:
471                         self.messagesUpdated.emit()
472                 if updateHistory:
473                         self.historyUpdated.emit()
474                 if oldDnd != self._dnd:
475                         self.dndStateChange.emit(self._dnd)
476                 if oldCallback != self._callback:
477                         self.callbackNumberChanged.emit(self._callback)
478
479                 self._save_to_cache()
480
481         def _update_contacts(self):
482                 try:
483                         self._contacts = yield (
484                                 self._backend[0].get_contacts,
485                                 (),
486                                 {},
487                         )
488                 except Exception, e:
489                         self.error.emit(str(e))
490                         return
491                 self.contactsUpdated.emit()
492
493         def _update_messages(self):
494                 try:
495                         self._messages = yield (
496                                 self._backend[0].get_messages,
497                                 (),
498                                 {},
499                         )
500                 except Exception, e:
501                         self.error.emit(str(e))
502                         return
503                 self.messagesUpdated.emit()
504
505         def _update_history(self):
506                 try:
507                         self._history = yield (
508                                 self._backend[0].get_recent,
509                                 (),
510                                 {},
511                         )
512                 except Exception, e:
513                         self.error.emit(str(e))
514                         return
515                 self.historyUpdated.emit()
516
517         def _update_dnd(self):
518                 oldDnd = self._dnd
519                 try:
520                         self._dnd = yield (
521                                 self._backend[0].is_dnd,
522                                 (),
523                                 {},
524                         )
525                 except Exception, e:
526                         self.error.emit(str(e))
527                         return
528                 if oldDnd != self._dnd:
529                         self.dndStateChange(self._dnd)
530
531         def _perform_op_while_loggedin(self, op):
532                 if self.state == self.LOGGEDIN_STATE:
533                         op.start()
534                 else:
535                         self._push_login_op(op)
536
537         def _push_login_op(self, asyncOp):
538                 assert self.state != self.LOGGEDIN_STATE
539                 if asyncOp in self._loginOps:
540                         _moduleLogger.info("Skipping queueing duplicate op: %r" % asyncOp)
541                         return
542                 self._loginOps.append(asyncOp)