Speeding up login through cookies
[theonering] / src / connection.py
1 import os
2 import weakref
3 import logging
4
5 import telepathy
6
7 import constants
8 import tp
9 import util.misc as misc_utils
10
11 import gvoice
12 import handle
13
14 import aliasing
15 import avatars
16 import capabilities
17 import contacts
18 import presence
19 import requests
20 import simple_presence
21
22 import autogv
23 import channel_manager
24
25
26 _moduleLogger = logging.getLogger(__name__)
27
28
29 class TheOneRingOptions(object):
30
31         useGVContacts = True
32
33         assert gvoice.session.Session._DEFAULTS["contacts"][1] == "hours"
34         contactsPollPeriodInHours = gvoice.session.Session._DEFAULTS["contacts"][0]
35
36         assert gvoice.session.Session._DEFAULTS["voicemail"][1] == "minutes"
37         voicemailPollPeriodInMinutes = gvoice.session.Session._DEFAULTS["voicemail"][0]
38
39         assert gvoice.session.Session._DEFAULTS["texts"][1] == "minutes"
40         textsPollPeriodInMinutes = gvoice.session.Session._DEFAULTS["texts"][0]
41
42         def __init__(self, parameters = None):
43                 if parameters is None:
44                         return
45                 self.useGVContacts = parameters["use-gv-contacts"]
46                 self.contactsPollPeriodInHours = parameters['contacts-poll-period-in-hours']
47                 self.voicemailPollPeriodInMinutes = parameters['voicemail-poll-period-in-minutes']
48                 self.textsPollPeriodInMinutes = parameters['texts-poll-period-in-minutes']
49
50
51 class TheOneRingConnection(
52         tp.Connection,
53         aliasing.AliasingMixin,
54         avatars.AvatarsMixin,
55         capabilities.CapabilitiesMixin,
56         contacts.ContactsMixin,
57         presence.PresenceMixin,
58         requests.RequestsMixin,
59         simple_presence.SimplePresenceMixin,
60 ):
61
62         # overiding base class variable
63         _mandatory_parameters = {
64                 'account': 's',
65                 'password': 's',
66         }
67         # overiding base class variable
68         _optional_parameters = {
69                 'forward': 's',
70                 'use-gv-contacts': 'b',
71                 'contacts-poll-period-in-hours': 'i',
72                 'voicemail-poll-period-in-minutes': 'i',
73                 'texts-poll-period-in-minutes': 'i',
74         }
75         _parameter_defaults = {
76                 'forward': '',
77                 'use-gv-contacts': TheOneRingOptions.useGVContacts,
78                 'contacts-poll-period-in-hours': TheOneRingOptions.contactsPollPeriodInHours,
79                 'voicemail-poll-period-in-minutes': TheOneRingOptions.voicemailPollPeriodInMinutes,
80                 'texts-poll-period-in-minutes': TheOneRingOptions.textsPollPeriodInMinutes,
81         }
82         _secret_parameters = set((
83                 "password",
84         ))
85
86         @misc_utils.log_exception(_moduleLogger)
87         def __init__(self, manager, parameters):
88                 self._loggers = []
89
90                 self.check_parameters(parameters)
91                 account = unicode(parameters['account'])
92                 encodedAccount = parameters['account'].encode('utf-8')
93                 encodedPassword = parameters['password'].encode('utf-8')
94                 encodedCallback = misc_utils.normalize_number(parameters['forward'].encode('utf-8'))
95                 if encodedCallback and not misc_utils.is_valid_number(encodedCallback):
96                         raise telepathy.errors.InvalidArgument("Invalid forwarding number")
97
98                 # Connection init must come first
99                 self.__options = TheOneRingOptions(parameters)
100                 self.__session = gvoice.session.Session(
101                         cookiePath = os.path.join(constants._data_path_, "%s.cookies" % account),
102                         defaults = {
103                                 "contacts": (self.__options.contactsPollPeriodInHours, "hours"),
104                                 "voicemail": (self.__options.voicemailPollPeriodInMinutes, "minutes"),
105                                 "texts": (self.__options.textsPollPeriodInMinutes, "minutes"),
106                         },
107                 )
108                 tp.Connection.__init__(
109                         self,
110                         constants._telepathy_protocol_name_,
111                         account,
112                         constants._telepathy_implementation_name_
113                 )
114                 aliasing.AliasingMixin.__init__(self)
115                 avatars.AvatarsMixin.__init__(self)
116                 capabilities.CapabilitiesMixin.__init__(self)
117                 contacts.ContactsMixin.__init__(self)
118                 presence.PresenceMixin.__init__(self)
119                 requests.RequestsMixin.__init__(self)
120                 simple_presence.SimplePresenceMixin.__init__(self)
121
122                 self.__manager = weakref.proxy(manager)
123                 self.__credentials = (
124                         encodedAccount,
125                         encodedPassword,
126                 )
127                 self.__callbackNumberParameter = encodedCallback
128                 self.__channelManager = channel_manager.ChannelManager(self)
129
130                 self.__cachePath = os.sep.join((constants._data_path_, "cache", self.username))
131                 try:
132                         os.makedirs(self.__cachePath)
133                 except OSError, e:
134                         if e.errno != 17:
135                                 raise
136
137                 self.set_self_handle(handle.create_handle(self, 'connection'))
138                 self._plumbing = [
139                         autogv.NewGVConversations(weakref.ref(self)),
140                         autogv.RefreshVoicemail(weakref.ref(self)),
141                         autogv.AutoDisconnect(weakref.ref(self)),
142                         autogv.DelayEnableContactIntegration(constants._telepathy_implementation_name_),
143                 ]
144
145                 _moduleLogger.info("Connection to the account %s created" % account)
146                 self._timedDisconnect = autogv.TimedDisconnect(weakref.ref(self))
147                 self._timedDisconnect.start()
148
149         @property
150         def manager(self):
151                 return self.__manager
152
153         @property
154         def session(self):
155                 return self.__session
156
157         @property
158         def options(self):
159                 return self.__options
160
161         @property
162         def username(self):
163                 return self.__credentials[0]
164
165         @property
166         def callbackNumberParameter(self):
167                 return self.__callbackNumberParameter
168
169         def get_handle_by_name(self, handleType, handleName):
170                 requestedHandleName = handleName.encode('utf-8')
171
172                 # We need to return an existing or create a new handle.  Unfortunately
173                 # handle init's take care of normalizing the handle name.  So we have
174                 # to create a new handle regardless and burn some handle id's and burn
175                 # some extra memory of creating objects we throw away if the handle
176                 # already exists.
177                 if handleType == telepathy.HANDLE_TYPE_CONTACT:
178                         h = handle.create_handle(self, 'contact', requestedHandleName)
179                 elif handleType == telepathy.HANDLE_TYPE_LIST:
180                         # Support only server side (immutable) lists
181                         h = handle.create_handle(self, 'list', requestedHandleName)
182                 else:
183                         raise telepathy.errors.NotAvailable('Handle type unsupported %d' % handleType)
184
185                 for candidate in self._handles.itervalues():
186                         if candidate.get_name() == h.get_name():
187                                 h = candidate
188                                 _moduleLogger.debug("Re-used handle for %s, I hoped this helped" % handleName)
189                                 break
190
191                 return h
192
193         def log_to_user(self, component, message):
194                 for logger in self._loggers:
195                         logger.log_message(component, message)
196
197         def add_logger(self, logger):
198                 self._loggers.append(logger)
199
200         def remove_logger(self, logger):
201                 self._loggers.remove(logger)
202
203         @property
204         def _channel_manager(self):
205                 return self.__channelManager
206
207         @misc_utils.log_exception(_moduleLogger)
208         def Connect(self):
209                 """
210                 For org.freedesktop.telepathy.Connection
211                 """
212                 if self._status != telepathy.CONNECTION_STATUS_DISCONNECTED:
213                         _moduleLogger.info("Attempting connect when not disconnected")
214                         return
215                 _moduleLogger.info("Connecting...")
216                 self.StatusChanged(
217                         telepathy.CONNECTION_STATUS_CONNECTING,
218                         telepathy.CONNECTION_STATUS_REASON_REQUESTED
219                 )
220                 self._timedDisconnect.stop()
221                 self.session.login(
222                         self.__credentials[0],
223                         self.__credentials[1],
224                         self._on_login,
225                         self._on_login_error,
226                 )
227
228         @misc_utils.log_exception(_moduleLogger)
229         def _on_login(self, *args):
230                 _moduleLogger.info("Connected, setting up...")
231                 try:
232                         self.__session.load(self.__cachePath)
233
234                         for plumber in self._plumbing:
235                                 plumber.start()
236                         if not self.__callbackNumberParameter:
237                                 callback = gvoice.backend.get_sane_callback(
238                                         self.session.backend
239                                 )
240                                 self.__callbackNumberParameter = misc_utils.normalize_number(callback)
241                         self.session.backend.set_callback_number(self.__callbackNumberParameter)
242
243                         subscribeHandle = self.get_handle_by_name(telepathy.HANDLE_TYPE_LIST, "subscribe")
244                         subscribeProps = self.generate_props(telepathy.CHANNEL_TYPE_CONTACT_LIST, subscribeHandle, False)
245                         self.__channelManager.channel_for_props(subscribeProps, signal=True)
246                         publishHandle = self.get_handle_by_name(telepathy.HANDLE_TYPE_LIST, "publish")
247                         publishProps = self.generate_props(telepathy.CHANNEL_TYPE_CONTACT_LIST, publishHandle, False)
248                         self.__channelManager.channel_for_props(publishProps, signal=True)
249                 except Exception:
250                         _moduleLogger.exception("Setup failed")
251                         self.disconnect(telepathy.CONNECTION_STATUS_REASON_AUTHENTICATION_FAILED)
252                         return
253
254                 _moduleLogger.info("Connected and set up")
255                 self.StatusChanged(
256                         telepathy.CONNECTION_STATUS_CONNECTED,
257                         telepathy.CONNECTION_STATUS_REASON_REQUESTED
258                 )
259
260         @misc_utils.log_exception(_moduleLogger)
261         def _on_login_error(self, error):
262                 _moduleLogger.error(error)
263                 if isinstance(error, StopIteration):
264                         pass
265                 elif isinstance(error, gvoice.backend.NetworkError):
266                         self.disconnect(telepathy.CONNECTION_STATUS_REASON_NETWORK_ERROR)
267                 else:
268                         self.disconnect(telepathy.CONNECTION_STATUS_REASON_AUTHENTICATION_FAILED)
269
270         @misc_utils.log_exception(_moduleLogger)
271         def Disconnect(self):
272                 """
273                 For org.freedesktop.telepathy.Connection
274                 """
275                 _moduleLogger.info("Kicking off disconnect")
276                 self.disconnect(telepathy.CONNECTION_STATUS_REASON_REQUESTED)
277
278         @misc_utils.log_exception(_moduleLogger)
279         def RequestChannel(self, type, handleType, handleId, suppressHandler):
280                 """
281                 For org.freedesktop.telepathy.Connection
282
283                 @param type DBus interface name for base channel type
284                 @param handleId represents a contact, list, etc according to handleType
285
286                 @returns DBus object path for the channel created or retrieved
287                 """
288                 self.check_connected()
289                 self.check_handle(handleType, handleId)
290
291                 h = self.get_handle_by_id(handleType, handleId) if handleId != 0 else None
292                 props = self.generate_props(type, h, suppressHandler)
293                 self._validate_handle(props)
294
295                 chan = self.__channelManager.channel_for_props(props, signal=True)
296                 path = chan._object_path
297                 _moduleLogger.info("RequestChannel Object Path (%s): %s" % (type.rsplit(".", 1)[-1], path))
298                 return path
299
300         def generate_props(self, channelType, handleObj, suppressHandler, initiatorHandle=None):
301                 targetHandle = 0 if handleObj is None else handleObj.get_id()
302                 targetHandleType = telepathy.HANDLE_TYPE_NONE if handleObj is None else handleObj.get_type()
303                 props = {
304                         telepathy.CHANNEL_INTERFACE + '.ChannelType': channelType,
305                         telepathy.CHANNEL_INTERFACE + '.TargetHandle': targetHandle,
306                         telepathy.CHANNEL_INTERFACE + '.TargetHandleType': targetHandleType,
307                         telepathy.CHANNEL_INTERFACE + '.Requested': suppressHandler
308                 }
309
310                 if initiatorHandle is not None:
311                         props[telepathy.CHANNEL_INTERFACE + '.InitiatorHandle'] = initiatorHandle.id
312
313                 return props
314
315         def disconnect(self, reason):
316                 _moduleLogger.info("Disconnecting")
317
318                 self._timedDisconnect.stop()
319
320                 # Not having the disconnect first can cause weird behavior with clients
321                 # including not being able to reconnect or even crashing
322                 self.StatusChanged(
323                         telepathy.CONNECTION_STATUS_DISCONNECTED,
324                         reason,
325                 )
326
327                 for plumber in self._plumbing:
328                         plumber.stop()
329
330                 self.__channelManager.close()
331                 self.manager.disconnected(self)
332
333                 self.session.save(self.__cachePath)
334                 self.session.shutdown()
335                 self.session.close()
336
337                 # In case one of the above items takes too long (which it should never
338                 # do), we leave the starting of the shutdown-on-idle counter to the
339                 # very end
340                 self.manager.disconnect_completed()
341
342                 _moduleLogger.info("Disconnected")