Upon startup, starting geometric polling at the max rather than the min
[theonering] / src / connection.py
1 import os
2 import weakref
3 import logging
4
5 import gobject
6 import telepathy
7
8 try:
9         import conic as _conic
10         conic = _conic
11 except (ImportError, OSError):
12         conic = None
13
14 import constants
15 import tp
16 import util.coroutines as coroutines
17 import util.go_utils as gobject_utils
18 import util.misc as util_misc
19 import gtk_toolbox
20
21 import gvoice
22 import handle
23
24 import requests
25 import contacts
26 import aliasing
27 import simple_presence
28 import presence
29 import capabilities
30
31 import channel_manager
32
33
34 _moduleLogger = logging.getLogger("connection")
35
36
37 class TheOneRingOptions(object):
38
39         useGVContacts = True
40
41         assert gvoice.session.Session._DEFAULTS["contacts"][1] == "hours"
42         contactsPollPeriodInHours = gvoice.session.Session._DEFAULTS["contacts"][0]
43
44         assert gvoice.session.Session._DEFAULTS["voicemail"][1] == "minutes"
45         voicemailPollPeriodInMinutes = gvoice.session.Session._DEFAULTS["voicemail"][0]
46
47         assert gvoice.session.Session._DEFAULTS["texts"][1] == "minutes"
48         textsPollPeriodInMinutes = gvoice.session.Session._DEFAULTS["texts"][0]
49
50         def __init__(self, parameters = None):
51                 if parameters is None:
52                         return
53                 self.useGVContacts = parameters["use-gv-contacts"]
54                 self.contactsPollPeriodInHours = parameters['contacts-poll-period-in-hours']
55                 self.voicemailPollPeriodInMinutes = parameters['voicemail-poll-period-in-minutes']
56                 self.textsPollPeriodInMinutes = parameters['texts-poll-period-in-minutes']
57
58
59 class TheOneRingConnection(
60         tp.Connection,
61         requests.RequestsMixin,
62         contacts.ContactsMixin,
63         aliasing.AliasingMixin,
64         simple_presence.SimplePresenceMixin,
65         presence.PresenceMixin,
66         capabilities.CapabilitiesMixin,
67 ):
68
69         # overiding base class variable
70         _mandatory_parameters = {
71                 'account': 's',
72                 'password': 's',
73         }
74         # overiding base class variable
75         _optional_parameters = {
76                 'forward': 's',
77                 'use-gv-contacts': 'b',
78                 'contacts-poll-period-in-hours': 'i',
79                 'voicemail-poll-period-in-minutes': 'i',
80                 'texts-poll-period-in-minutes': 'i',
81         }
82         _parameter_defaults = {
83                 'forward': '',
84                 'use-gv-contacts': TheOneRingOptions.useGVContacts,
85                 'contacts-poll-period-in-hours': TheOneRingOptions.contactsPollPeriodInHours,
86                 'voicemail-poll-period-in-minutes': TheOneRingOptions.voicemailPollPeriodInMinutes,
87                 'texts-poll-period-in-minutes': TheOneRingOptions.textsPollPeriodInMinutes,
88         }
89         _secret_parameters = set((
90                 "password",
91         ))
92
93         @gtk_toolbox.log_exception(_moduleLogger)
94         def __init__(self, manager, parameters):
95                 self.check_parameters(parameters)
96                 account = unicode(parameters['account'])
97                 encodedAccount = parameters['account'].encode('utf-8')
98                 encodedPassword = parameters['password'].encode('utf-8')
99                 encodedCallback = util_misc.normalize_number(parameters['forward'].encode('utf-8'))
100                 if encodedCallback and not util_misc.is_valid_number(encodedCallback):
101                         raise telepathy.errors.InvalidArgument("Invalid forwarding number")
102
103                 # Connection init must come first
104                 self.__session = gvoice.session.Session(None)
105                 self.__options = TheOneRingOptions(parameters)
106                 tp.Connection.__init__(
107                         self,
108                         constants._telepathy_protocol_name_,
109                         account,
110                         constants._telepathy_implementation_name_
111                 )
112                 requests.RequestsMixin.__init__(self)
113                 contacts.ContactsMixin.__init__(self)
114                 aliasing.AliasingMixin.__init__(self)
115                 simple_presence.SimplePresenceMixin.__init__(self)
116                 presence.PresenceMixin.__init__(self)
117                 capabilities.CapabilitiesMixin.__init__(self)
118
119                 self.__manager = weakref.proxy(manager)
120                 self.__credentials = (
121                         encodedAccount,
122                         encodedPassword,
123                 )
124                 self.__callbackNumberParameter = encodedCallback
125                 self.__channelManager = channel_manager.ChannelManager(self)
126
127                 if conic is not None:
128                         self.__connection = conic.Connection()
129                 else:
130                         self.__connection = None
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
140                 self.__callback = None
141                 self.__connectionEventId = None
142                 self.__delayedDisconnectEventId = None
143                 _moduleLogger.info("Connection to the account %s created" % account)
144
145         @property
146         def manager(self):
147                 return self.__manager
148
149         @property
150         def session(self):
151                 return self.__session
152
153         @property
154         def options(self):
155                 return self.__options
156
157         @property
158         def username(self):
159                 return self.__credentials[0]
160
161         @property
162         def callbackNumberParameter(self):
163                 return self.__callbackNumberParameter
164
165         def get_handle_by_name(self, handleType, handleName):
166                 requestedHandleName = handleName.encode('utf-8')
167                 if handleType == telepathy.HANDLE_TYPE_CONTACT:
168                         _moduleLogger.debug("get_handle_by_name Contact: %s" % requestedHandleName)
169                         h = handle.create_handle(self, 'contact', requestedHandleName)
170                 elif handleType == telepathy.HANDLE_TYPE_LIST:
171                         # Support only server side (immutable) lists
172                         _moduleLogger.debug("get_handle_by_name List: %s" % requestedHandleName)
173                         h = handle.create_handle(self, 'list', requestedHandleName)
174                 else:
175                         raise telepathy.errors.NotAvailable('Handle type unsupported %d' % handleType)
176                 return h
177
178         @property
179         def _channel_manager(self):
180                 return self.__channelManager
181
182         @gtk_toolbox.log_exception(_moduleLogger)
183         def Connect(self):
184                 """
185                 For org.freedesktop.telepathy.Connection
186                 """
187                 _moduleLogger.info("Connecting...")
188                 self.StatusChanged(
189                         telepathy.CONNECTION_STATUS_CONNECTING,
190                         telepathy.CONNECTION_STATUS_REASON_REQUESTED
191                 )
192                 try:
193                         self.__session.load(self.__cachePath)
194
195                         self.__callback = coroutines.func_sink(
196                                 coroutines.expand_positional(
197                                         self._on_conversations_updated
198                                 )
199                         )
200                         self.session.voicemails.updateSignalHandler.register_sink(
201                                 self.__callback
202                         )
203                         self.session.texts.updateSignalHandler.register_sink(
204                                 self.__callback
205                         )
206                         self.session.login(*self.__credentials)
207                         if not self.__callbackNumberParameter:
208                                 callback = gvoice.backend.get_sane_callback(
209                                         self.session.backend
210                                 )
211                                 self.__callbackNumberParameter = util_misc.normalize_number(callback)
212                         self.session.backend.set_callback_number(self.__callbackNumberParameter)
213
214                         subscribeHandle = self.get_handle_by_name(telepathy.HANDLE_TYPE_LIST, "subscribe")
215                         subscribeProps = self._generate_props(telepathy.CHANNEL_TYPE_CONTACT_LIST, subscribeHandle, False)
216                         self.__channelManager.channel_for_props(subscribeProps, signal=True)
217                         publishHandle = self.get_handle_by_name(telepathy.HANDLE_TYPE_LIST, "publish")
218                         publishProps = self._generate_props(telepathy.CHANNEL_TYPE_CONTACT_LIST, publishHandle, False)
219                         self.__channelManager.channel_for_props(publishProps, signal=True)
220                 except gvoice.backend.NetworkError, e:
221                         _moduleLogger.exception("Connection Failed")
222                         self.StatusChanged(
223                                 telepathy.CONNECTION_STATUS_DISCONNECTED,
224                                 telepathy.CONNECTION_STATUS_REASON_NETWORK_ERROR
225                         )
226                         return
227                 except Exception, e:
228                         _moduleLogger.exception("Connection Failed")
229                         self.StatusChanged(
230                                 telepathy.CONNECTION_STATUS_DISCONNECTED,
231                                 telepathy.CONNECTION_STATUS_REASON_AUTHENTICATION_FAILED
232                         )
233                         return
234
235                 _moduleLogger.info("Connected")
236                 self.StatusChanged(
237                         telepathy.CONNECTION_STATUS_CONNECTED,
238                         telepathy.CONNECTION_STATUS_REASON_REQUESTED
239                 )
240                 if self.__connection is not None:
241                         self.__connectionEventId = self.__connection.connect("connection-event", self._on_connection_change)
242
243         @gtk_toolbox.log_exception(_moduleLogger)
244         def Disconnect(self):
245                 """
246                 For org.freedesktop.telepathy.Connection
247                 """
248                 try:
249                         self._disconnect()
250                 except Exception:
251                         _moduleLogger.exception("Error durring disconnect")
252                 self.StatusChanged(
253                         telepathy.CONNECTION_STATUS_DISCONNECTED,
254                         telepathy.CONNECTION_STATUS_REASON_REQUESTED
255                 )
256
257         @gtk_toolbox.log_exception(_moduleLogger)
258         def RequestChannel(self, type, handleType, handleId, suppressHandler):
259                 """
260                 For org.freedesktop.telepathy.Connection
261
262                 @param type DBus interface name for base channel type
263                 @param handleId represents a contact, list, etc according to handleType
264
265                 @returns DBus object path for the channel created or retrieved
266                 """
267                 self.check_connected()
268                 self.check_handle(handleType, handleId)
269
270                 h = self.get_handle_by_id(handleType, handleId) if handleId != 0 else None
271                 props = self._generate_props(type, h, suppressHandler)
272                 self._validate_handle(props)
273
274                 chan = self.__channelManager.channel_for_props(props, signal=True)
275                 path = chan._object_path
276                 _moduleLogger.info("RequestChannel Object Path (%s): %s" % (type.rsplit(".", 1)[-1], path))
277                 return path
278
279         def _generate_props(self, channelType, handle, suppressHandler, initiatorHandle=None):
280                 targetHandle = 0 if handle is None else handle.get_id()
281                 targetHandleType = telepathy.HANDLE_TYPE_NONE if handle is None else handle.get_type()
282                 props = {
283                         telepathy.CHANNEL_INTERFACE + '.ChannelType': channelType,
284                         telepathy.CHANNEL_INTERFACE + '.TargetHandle': targetHandle,
285                         telepathy.CHANNEL_INTERFACE + '.TargetHandleType': targetHandleType,
286                         telepathy.CHANNEL_INTERFACE + '.Requested': suppressHandler
287                 }
288
289                 if initiatorHandle is not None:
290                         props[telepathy.CHANNEL_INTERFACE + '.InitiatorHandle'] = initiatorHandle.id
291
292                 return props
293
294         def _disconnect(self):
295                 _moduleLogger.info("Disconnecting")
296                 self.session.voicemails.updateSignalHandler.unregister_sink(
297                         self.__callback
298                 )
299                 self.session.texts.updateSignalHandler.unregister_sink(
300                         self.__callback
301                 )
302                 self.__callback = None
303
304                 self.__channelManager.close()
305                 self.session.save(self.__cachePath)
306                 self.session.logout()
307                 self.session.close()
308
309                 self.manager.disconnected(self)
310
311                 self._cancel_delayed_disconnect()
312                 self.__connection = None
313                 _moduleLogger.info("Disconnected")
314
315         @gtk_toolbox.log_exception(_moduleLogger)
316         def _on_conversations_updated(self, conv, conversationIds):
317                 _moduleLogger.debug("Incoming messages from: %r" % (conversationIds, ))
318                 for phoneNumber in conversationIds:
319                         h = self.get_handle_by_name(telepathy.HANDLE_TYPE_CONTACT, phoneNumber)
320                         # Just let the TextChannel decide whether it should be reported to the user or not
321                         props = self._generate_props(telepathy.CHANNEL_TYPE_TEXT, h, False)
322                         if self.__channelManager.channel_exists(props):
323                                 continue
324
325                         # Maemo 4.1's RTComm opens a window for a chat regardless if a
326                         # message is received or not, so we need to do some filtering here
327                         mergedConv = conv.get_conversation(phoneNumber)
328                         unreadConvs = [
329                                 conversation
330                                 for conversation in mergedConv.conversations
331                                 if not conversation.isRead and not conversation.isArchived
332                         ]
333                         if not unreadConvs:
334                                 continue
335
336                         chan = self.__channelManager.channel_for_props(props, signal=True)
337
338         @gtk_toolbox.log_exception(_moduleLogger)
339         def _on_connection_change(self, connection, event):
340                 """
341                 @note Maemo specific
342                 """
343                 status = event.get_status()
344                 error = event.get_error()
345                 iap_id = event.get_iap_id()
346                 bearer = event.get_bearer_type()
347
348                 if status == conic.STATUS_DISCONNECTED:
349                         _moduleLogger.info("Disconnected from network, starting countdown to logoff")
350                         self.__delayedDisconnectEventId = gobject_utils.timeout_add_seconds(
351                                 5, self._on_delayed_disconnect
352                         )
353                 elif status == conic.STATUS_CONNECTED:
354                         _moduleLogger.info("Connected to network")
355                         self._cancel_delayed_disconnect()
356                 else:
357                         _moduleLogger.info("Other status: %r" % (status, ))
358
359         def _cancel_delayed_disconnect(self):
360                 if self.__delayedDisconnectEventId is None:
361                         return
362                 _moduleLogger.info("Cancelling auto-log off")
363                 gobject.source_reove(self.__delayedDisconnectEventId)
364                 self.__delayedDisconnectEventId = None
365
366         @gtk_toolbox.log_exception(_moduleLogger)
367         def _on_delayed_disconnect(self):
368                 if not self.session.is_logged_in():
369                         _moduleLogger.info("Received connection change event when not logged in")
370                         return
371                 try:
372                         self._disconnect()
373                 except Exception:
374                         _moduleLogger.exception("Error durring disconnect")
375                 self.StatusChanged(
376                         telepathy.CONNECTION_STATUS_DISCONNECTED,
377                         telepathy.CONNECTION_STATUS_REASON_NETWORK_ERROR
378                 )
379                 self.__delayedDisconnectEventId = None
380                 return False