Imitiating buttfly in being explicitly typed
[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.check_parameters(parameters)
89                 account = unicode(parameters['account'])
90                 encodedAccount = parameters['account'].encode('utf-8')
91                 encodedPassword = parameters['password'].encode('utf-8')
92                 encodedCallback = misc_utils.normalize_number(parameters['forward'].encode('utf-8'))
93                 if encodedCallback and not misc_utils.is_valid_number(encodedCallback):
94                         raise telepathy.errors.InvalidArgument("Invalid forwarding number")
95
96                 # Connection init must come first
97                 self.__options = TheOneRingOptions(parameters)
98                 self.__session = gvoice.session.Session(
99                         cookiePath = None,
100                         defaults = {
101                                 "contacts": (self.__options.contactsPollPeriodInHours, "hours"),
102                                 "voicemail": (self.__options.voicemailPollPeriodInMinutes, "minutes"),
103                                 "texts": (self.__options.textsPollPeriodInMinutes, "minutes"),
104                         },
105                 )
106                 tp.Connection.__init__(
107                         self,
108                         constants._telepathy_protocol_name_,
109                         account,
110                         constants._telepathy_implementation_name_
111                 )
112                 aliasing.AliasingMixin.__init__(self)
113                 avatars.AvatarsMixin.__init__(self)
114                 capabilities.CapabilitiesMixin.__init__(self)
115                 contacts.ContactsMixin.__init__(self)
116                 presence.PresenceMixin.__init__(self)
117                 requests.RequestsMixin.__init__(self)
118                 simple_presence.SimplePresenceMixin.__init__(self)
119
120                 self.__manager = weakref.proxy(manager)
121                 self.__credentials = (
122                         encodedAccount,
123                         encodedPassword,
124                 )
125                 self.__callbackNumberParameter = encodedCallback
126                 self.__channelManager = channel_manager.ChannelManager(self)
127
128                 self.__cachePath = os.sep.join((constants._data_path_, "cache", self.username))
129                 try:
130                         os.makedirs(self.__cachePath)
131                 except OSError, e:
132                         if e.errno != 17:
133                                 raise
134
135                 self.set_self_handle(handle.create_handle(self, 'connection'))
136                 self._plumbing = [
137                         autogv.NewGVConversations(weakref.ref(self)),
138                         autogv.RefreshVoicemail(weakref.ref(self)),
139                         autogv.AutoDisconnect(weakref.ref(self)),
140                         autogv.DelayEnableContactIntegration(constants._telepathy_implementation_name_),
141                 ]
142
143                 _moduleLogger.info("Connection to the account %s created" % account)
144                 self._timedDisconnect = autogv.TimedDisconnect(weakref.ref(self))
145                 self._timedDisconnect.start()
146
147         @property
148         def manager(self):
149                 return self.__manager
150
151         @property
152         def session(self):
153                 return self.__session
154
155         @property
156         def options(self):
157                 return self.__options
158
159         @property
160         def username(self):
161                 return self.__credentials[0]
162
163         @property
164         def callbackNumberParameter(self):
165                 return self.__callbackNumberParameter
166
167         def get_handle_by_name(self, handleType, handleName):
168                 requestedHandleName = handleName.encode('utf-8')
169
170                 # We need to return an existing or create a new handle.  Unfortunately
171                 # handle init's take care of normalizing the handle name.  So we have
172                 # to create a new handle regardless and burn some handle id's and burn
173                 # some extra memory of creating objects we throw away if the handle
174                 # already exists.
175                 if handleType == telepathy.HANDLE_TYPE_CONTACT:
176                         h = handle.create_handle(self, 'contact', requestedHandleName)
177                 elif handleType == telepathy.HANDLE_TYPE_LIST:
178                         # Support only server side (immutable) lists
179                         h = handle.create_handle(self, 'list', requestedHandleName)
180                 else:
181                         raise telepathy.errors.NotAvailable('Handle type unsupported %d' % handleType)
182
183                 for candidate in self._handles.itervalues():
184                         if candidate.get_name() == h.get_name():
185                                 h = candidate
186                                 _moduleLogger.debug("Re-used handle for %s, I hoped this helped" % handleName)
187                                 break
188
189                 return h
190
191         @property
192         def _channel_manager(self):
193                 return self.__channelManager
194
195         @misc_utils.log_exception(_moduleLogger)
196         def Connect(self):
197                 """
198                 For org.freedesktop.telepathy.Connection
199                 """
200                 if self._status != telepathy.CONNECTION_STATUS_DISCONNECTED:
201                         _moduleLogger.info("Attempting connect when not disconnected")
202                         return
203                 _moduleLogger.info("Connecting...")
204                 self.StatusChanged(
205                         telepathy.CONNECTION_STATUS_CONNECTING,
206                         telepathy.CONNECTION_STATUS_REASON_REQUESTED
207                 )
208                 self._timedDisconnect.stop()
209                 self.session.login(
210                         self.__credentials[0],
211                         self.__credentials[1],
212                         self._on_login,
213                         self._on_login_error,
214                 )
215
216         @misc_utils.log_exception(_moduleLogger)
217         def _on_login(self, *args):
218                 _moduleLogger.info("Connected, setting up...")
219                 try:
220                         self.__session.load(self.__cachePath)
221
222                         for plumber in self._plumbing:
223                                 plumber.start()
224                         if not self.__callbackNumberParameter:
225                                 callback = gvoice.backend.get_sane_callback(
226                                         self.session.backend
227                                 )
228                                 self.__callbackNumberParameter = misc_utils.normalize_number(callback)
229                         self.session.backend.set_callback_number(self.__callbackNumberParameter)
230
231                         subscribeHandle = self.get_handle_by_name(telepathy.HANDLE_TYPE_LIST, "subscribe")
232                         subscribeProps = self.generate_props(telepathy.CHANNEL_TYPE_CONTACT_LIST, subscribeHandle, False)
233                         self.__channelManager.channel_for_props(subscribeProps, signal=True)
234                         publishHandle = self.get_handle_by_name(telepathy.HANDLE_TYPE_LIST, "publish")
235                         publishProps = self.generate_props(telepathy.CHANNEL_TYPE_CONTACT_LIST, publishHandle, False)
236                         self.__channelManager.channel_for_props(publishProps, signal=True)
237                 except Exception:
238                         _moduleLogger.exception("Setup failed")
239                         self.disconnect(telepathy.CONNECTION_STATUS_REASON_AUTHENTICATION_FAILED)
240                         return
241
242                 _moduleLogger.info("Connected and set up")
243                 self.StatusChanged(
244                         telepathy.CONNECTION_STATUS_CONNECTED,
245                         telepathy.CONNECTION_STATUS_REASON_REQUESTED
246                 )
247
248         @misc_utils.log_exception(_moduleLogger)
249         def _on_login_error(self, error):
250                 _moduleLogger.error(error)
251                 if isinstance(error, StopIteration):
252                         pass
253                 elif isinstance(error, gvoice.backend.NetworkError):
254                         self.disconnect(telepathy.CONNECTION_STATUS_REASON_NETWORK_ERROR)
255                 else:
256                         self.disconnect(telepathy.CONNECTION_STATUS_REASON_AUTHENTICATION_FAILED)
257
258         @misc_utils.log_exception(_moduleLogger)
259         def Disconnect(self):
260                 """
261                 For org.freedesktop.telepathy.Connection
262                 """
263                 _moduleLogger.info("Kicking off disconnect")
264                 self.disconnect(telepathy.CONNECTION_STATUS_REASON_REQUESTED)
265
266         @misc_utils.log_exception(_moduleLogger)
267         def RequestChannel(self, type, handleType, handleId, suppressHandler):
268                 """
269                 For org.freedesktop.telepathy.Connection
270
271                 @param type DBus interface name for base channel type
272                 @param handleId represents a contact, list, etc according to handleType
273
274                 @returns DBus object path for the channel created or retrieved
275                 """
276                 self.check_connected()
277                 self.check_handle(handleType, handleId)
278
279                 h = self.get_handle_by_id(handleType, handleId) if handleId != 0 else None
280                 props = self.generate_props(type, h, suppressHandler)
281                 self._validate_handle(props)
282
283                 chan = self.__channelManager.channel_for_props(props, signal=True)
284                 path = chan._object_path
285                 _moduleLogger.info("RequestChannel Object Path (%s): %s" % (type.rsplit(".", 1)[-1], path))
286                 return path
287
288         def generate_props(self, channelType, handleObj, suppressHandler, initiatorHandle=None):
289                 targetHandle = 0 if handleObj is None else handleObj.get_id()
290                 targetHandleType = telepathy.HANDLE_TYPE_NONE if handleObj is None else handleObj.get_type()
291                 props = {
292                         telepathy.CHANNEL_INTERFACE + '.ChannelType': channelType,
293                         telepathy.CHANNEL_INTERFACE + '.TargetHandle': targetHandle,
294                         telepathy.CHANNEL_INTERFACE + '.TargetHandleType': targetHandleType,
295                         telepathy.CHANNEL_INTERFACE + '.Requested': suppressHandler
296                 }
297
298                 if initiatorHandle is not None:
299                         props[telepathy.CHANNEL_INTERFACE + '.InitiatorHandle'] = initiatorHandle.id
300
301                 return props
302
303         def disconnect(self, reason):
304                 _moduleLogger.info("Disconnecting")
305
306                 self._timedDisconnect.stop()
307
308                 # Not having the disconnect first can cause weird behavior with clients
309                 # including not being able to reconnect or even crashing
310                 self.StatusChanged(
311                         telepathy.CONNECTION_STATUS_DISCONNECTED,
312                         reason,
313                 )
314
315                 for plumber in self._plumbing:
316                         plumber.stop()
317
318                 self.__channelManager.close()
319                 self.manager.disconnected(self)
320
321                 self.session.save(self.__cachePath)
322                 self.session.logout()
323                 self.session.close()
324
325                 # In case one of the above items takes too long (which it should never
326                 # do), we leave the starting of the shutdown-on-idle counter to the
327                 # very end
328                 self.manager.disconnect_completed()
329
330                 _moduleLogger.info("Disconnected")