Removing some debug stuff
[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.__options = TheOneRingOptions(parameters)
105                 self.__session = gvoice.session.Session(
106                         cookiePath = None,
107                         defaults = {
108                                 "contacts": (self.__options.contactsPollPeriodInHours, "hours"),
109                                 "voicemail": (self.__options.voicemailPollPeriodInMinutes, "minutes"),
110                                 "texts": (self.__options.textsPollPeriodInMinutes, "minutes"),
111                         },
112                 )
113                 tp.Connection.__init__(
114                         self,
115                         constants._telepathy_protocol_name_,
116                         account,
117                         constants._telepathy_implementation_name_
118                 )
119                 requests.RequestsMixin.__init__(self)
120                 contacts.ContactsMixin.__init__(self)
121                 aliasing.AliasingMixin.__init__(self)
122                 simple_presence.SimplePresenceMixin.__init__(self)
123                 presence.PresenceMixin.__init__(self)
124                 capabilities.CapabilitiesMixin.__init__(self)
125
126                 self.__manager = weakref.proxy(manager)
127                 self.__credentials = (
128                         encodedAccount,
129                         encodedPassword,
130                 )
131                 self.__callbackNumberParameter = encodedCallback
132                 self.__channelManager = channel_manager.ChannelManager(self)
133
134                 if conic is not None:
135                         self.__connection = conic.Connection()
136                 else:
137                         self.__connection = None
138                 self.__cachePath = os.sep.join((constants._data_path_, "cache", self.username))
139                 try:
140                         os.makedirs(self.__cachePath)
141                 except OSError, e:
142                         if e.errno != 17:
143                                 raise
144
145                 self.set_self_handle(handle.create_handle(self, 'connection'))
146
147                 self.__callback = None
148                 self.__connectionEventId = None
149                 self.__delayedDisconnectEventId = None
150                 _moduleLogger.info("Connection to the account %s created" % account)
151
152         @property
153         def manager(self):
154                 return self.__manager
155
156         @property
157         def session(self):
158                 return self.__session
159
160         @property
161         def options(self):
162                 return self.__options
163
164         @property
165         def username(self):
166                 return self.__credentials[0]
167
168         @property
169         def callbackNumberParameter(self):
170                 return self.__callbackNumberParameter
171
172         def get_handle_by_name(self, handleType, handleName):
173                 requestedHandleName = handleName.encode('utf-8')
174                 if handleType == telepathy.HANDLE_TYPE_CONTACT:
175                         _moduleLogger.debug("get_handle_by_name Contact: %s" % requestedHandleName)
176                         h = handle.create_handle(self, 'contact', requestedHandleName)
177                 elif handleType == telepathy.HANDLE_TYPE_LIST:
178                         # Support only server side (immutable) lists
179                         _moduleLogger.debug("get_handle_by_name List: %s" % requestedHandleName)
180                         h = handle.create_handle(self, 'list', requestedHandleName)
181                 else:
182                         raise telepathy.errors.NotAvailable('Handle type unsupported %d' % handleType)
183                 return h
184
185         @property
186         def _channel_manager(self):
187                 return self.__channelManager
188
189         @gtk_toolbox.log_exception(_moduleLogger)
190         def Connect(self):
191                 """
192                 For org.freedesktop.telepathy.Connection
193                 """
194                 _moduleLogger.info("Connecting...")
195                 self.StatusChanged(
196                         telepathy.CONNECTION_STATUS_CONNECTING,
197                         telepathy.CONNECTION_STATUS_REASON_REQUESTED
198                 )
199                 try:
200                         self.__session.load(self.__cachePath)
201
202                         self.__callback = coroutines.func_sink(
203                                 coroutines.expand_positional(
204                                         self._on_conversations_updated
205                                 )
206                         )
207                         self.session.voicemails.updateSignalHandler.register_sink(
208                                 self.__callback
209                         )
210                         self.session.texts.updateSignalHandler.register_sink(
211                                 self.__callback
212                         )
213                         self.session.login(*self.__credentials)
214                         if not self.__callbackNumberParameter:
215                                 callback = gvoice.backend.get_sane_callback(
216                                         self.session.backend
217                                 )
218                                 self.__callbackNumberParameter = util_misc.normalize_number(callback)
219                         self.session.backend.set_callback_number(self.__callbackNumberParameter)
220
221                         subscribeHandle = self.get_handle_by_name(telepathy.HANDLE_TYPE_LIST, "subscribe")
222                         subscribeProps = self._generate_props(telepathy.CHANNEL_TYPE_CONTACT_LIST, subscribeHandle, False)
223                         self.__channelManager.channel_for_props(subscribeProps, signal=True)
224                         publishHandle = self.get_handle_by_name(telepathy.HANDLE_TYPE_LIST, "publish")
225                         publishProps = self._generate_props(telepathy.CHANNEL_TYPE_CONTACT_LIST, publishHandle, False)
226                         self.__channelManager.channel_for_props(publishProps, signal=True)
227                 except gvoice.backend.NetworkError, e:
228                         _moduleLogger.exception("Connection Failed")
229                         self.StatusChanged(
230                                 telepathy.CONNECTION_STATUS_DISCONNECTED,
231                                 telepathy.CONNECTION_STATUS_REASON_NETWORK_ERROR
232                         )
233                         return
234                 except Exception, e:
235                         _moduleLogger.exception("Connection Failed")
236                         self.StatusChanged(
237                                 telepathy.CONNECTION_STATUS_DISCONNECTED,
238                                 telepathy.CONNECTION_STATUS_REASON_AUTHENTICATION_FAILED
239                         )
240                         return
241
242                 _moduleLogger.info("Connected")
243                 self.StatusChanged(
244                         telepathy.CONNECTION_STATUS_CONNECTED,
245                         telepathy.CONNECTION_STATUS_REASON_REQUESTED
246                 )
247                 if self.__connection is not None:
248                         self.__connectionEventId = self.__connection.connect("connection-event", self._on_connection_change)
249
250         @gtk_toolbox.log_exception(_moduleLogger)
251         def Disconnect(self):
252                 """
253                 For org.freedesktop.telepathy.Connection
254                 """
255                 try:
256                         self._disconnect()
257                 except Exception:
258                         _moduleLogger.exception("Error durring disconnect")
259                 self.StatusChanged(
260                         telepathy.CONNECTION_STATUS_DISCONNECTED,
261                         telepathy.CONNECTION_STATUS_REASON_REQUESTED
262                 )
263
264         @gtk_toolbox.log_exception(_moduleLogger)
265         def RequestChannel(self, type, handleType, handleId, suppressHandler):
266                 """
267                 For org.freedesktop.telepathy.Connection
268
269                 @param type DBus interface name for base channel type
270                 @param handleId represents a contact, list, etc according to handleType
271
272                 @returns DBus object path for the channel created or retrieved
273                 """
274                 self.check_connected()
275                 self.check_handle(handleType, handleId)
276
277                 h = self.get_handle_by_id(handleType, handleId) if handleId != 0 else None
278                 props = self._generate_props(type, h, suppressHandler)
279                 self._validate_handle(props)
280
281                 chan = self.__channelManager.channel_for_props(props, signal=True)
282                 path = chan._object_path
283                 _moduleLogger.info("RequestChannel Object Path (%s): %s" % (type.rsplit(".", 1)[-1], path))
284                 return path
285
286         def _generate_props(self, channelType, handle, suppressHandler, initiatorHandle=None):
287                 targetHandle = 0 if handle is None else handle.get_id()
288                 targetHandleType = telepathy.HANDLE_TYPE_NONE if handle is None else handle.get_type()
289                 props = {
290                         telepathy.CHANNEL_INTERFACE + '.ChannelType': channelType,
291                         telepathy.CHANNEL_INTERFACE + '.TargetHandle': targetHandle,
292                         telepathy.CHANNEL_INTERFACE + '.TargetHandleType': targetHandleType,
293                         telepathy.CHANNEL_INTERFACE + '.Requested': suppressHandler
294                 }
295
296                 if initiatorHandle is not None:
297                         props[telepathy.CHANNEL_INTERFACE + '.InitiatorHandle'] = initiatorHandle.id
298
299                 return props
300
301         def _disconnect(self):
302                 _moduleLogger.info("Disconnecting")
303                 self.session.voicemails.updateSignalHandler.unregister_sink(
304                         self.__callback
305                 )
306                 self.session.texts.updateSignalHandler.unregister_sink(
307                         self.__callback
308                 )
309                 self.__callback = None
310
311                 self.__channelManager.close()
312                 self.session.save(self.__cachePath)
313                 self.session.logout()
314                 self.session.close()
315
316                 self.manager.disconnected(self)
317
318                 self._cancel_delayed_disconnect()
319                 self.__connection = None
320                 _moduleLogger.info("Disconnected")
321
322         @gtk_toolbox.log_exception(_moduleLogger)
323         def _on_conversations_updated(self, conv, conversationIds):
324                 _moduleLogger.debug("Incoming messages from: %r" % (conversationIds, ))
325                 for phoneNumber in conversationIds:
326                         h = self.get_handle_by_name(telepathy.HANDLE_TYPE_CONTACT, phoneNumber)
327                         # Just let the TextChannel decide whether it should be reported to the user or not
328                         props = self._generate_props(telepathy.CHANNEL_TYPE_TEXT, h, False)
329                         if self.__channelManager.channel_exists(props):
330                                 continue
331
332                         # Maemo 4.1's RTComm opens a window for a chat regardless if a
333                         # message is received or not, so we need to do some filtering here
334                         mergedConv = conv.get_conversation(phoneNumber)
335                         unreadConvs = [
336                                 conversation
337                                 for conversation in mergedConv.conversations
338                                 if not conversation.isRead and not conversation.isArchived
339                         ]
340                         if not unreadConvs:
341                                 continue
342
343                         chan = self.__channelManager.channel_for_props(props, signal=True)
344
345         @gtk_toolbox.log_exception(_moduleLogger)
346         def _on_connection_change(self, connection, event):
347                 """
348                 @note Maemo specific
349                 """
350                 status = event.get_status()
351                 error = event.get_error()
352                 iap_id = event.get_iap_id()
353                 bearer = event.get_bearer_type()
354
355                 if status == conic.STATUS_DISCONNECTED:
356                         _moduleLogger.info("Disconnected from network, starting countdown to logoff")
357                         self.__delayedDisconnectEventId = gobject_utils.timeout_add_seconds(
358                                 5, self._on_delayed_disconnect
359                         )
360                 elif status == conic.STATUS_CONNECTED:
361                         _moduleLogger.info("Connected to network")
362                         self._cancel_delayed_disconnect()
363                 else:
364                         _moduleLogger.info("Other status: %r" % (status, ))
365
366         def _cancel_delayed_disconnect(self):
367                 if self.__delayedDisconnectEventId is None:
368                         return
369                 _moduleLogger.info("Cancelling auto-log off")
370                 gobject.source_reove(self.__delayedDisconnectEventId)
371                 self.__delayedDisconnectEventId = None
372
373         @gtk_toolbox.log_exception(_moduleLogger)
374         def _on_delayed_disconnect(self):
375                 if not self.session.is_logged_in():
376                         _moduleLogger.info("Received connection change event when not logged in")
377                         return
378                 try:
379                         self._disconnect()
380                 except Exception:
381                         _moduleLogger.exception("Error durring disconnect")
382                 self.StatusChanged(
383                         telepathy.CONNECTION_STATUS_DISCONNECTED,
384                         telepathy.CONNECTION_STATUS_REASON_NETWORK_ERROR
385                 )
386                 self.__delayedDisconnectEventId = None
387                 return False