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