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