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