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