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