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