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