Added detection of missed calls. As part of this I moved some of the connections...
[theonering] / src / tp / conn.py
1 # telepathy-python - Base classes defining the interfaces of the Telepathy framework
2 #
3 # Copyright (C) 2005, 2006 Collabora Limited
4 # Copyright (C) 2005, 2006 Nokia Corporation
5 # Copyright (C) 2006 INdT
6 #
7 # This library is free software; you can redistribute it and/or
8 # modify it under the terms of the GNU Lesser General Public
9 # License as published by the Free Software Foundation; either
10 # version 2.1 of the License, or (at your option) any later version.
11 #
12 # This library is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 # Lesser General Public License for more details.
16 #
17 # You should have received a copy of the GNU Lesser General Public
18 # License along with this library; if not, write to the Free Software
19 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
20
21 import dbus
22 import dbus.service
23 import re
24 import weakref
25
26 from telepathy.constants import (CONNECTION_STATUS_DISCONNECTED,
27                                  CONNECTION_STATUS_CONNECTED,
28                                  HANDLE_TYPE_NONE,
29                                  HANDLE_TYPE_CONTACT,
30                                  LAST_HANDLE_TYPE)
31 from telepathy.errors import (Disconnected, InvalidArgument,
32                               InvalidHandle, NotAvailable,
33                               NotImplemented)
34 from telepathy.interfaces import (CONN_INTERFACE,
35                                   CONN_INTERFACE_ALIASING,
36                                   CONN_INTERFACE_AVATARS,
37                                   CONN_INTERFACE_CAPABILITIES,
38                                   CONN_INTERFACE_PRESENCE,
39                                   CONN_INTERFACE_RENAMING,
40                                   CONNECTION_INTERFACE_REQUESTS,
41                                   CHANNEL_INTERFACE)
42 from handle import Handle
43 from properties import DBusProperties
44
45 from telepathy._generated.Connection import Connection as _Connection
46
47 _BAD = re.compile(r'(?:^[0-9])|(?:[^A-Za-z0-9])')
48
49 def _escape_as_identifier(name):
50     if isinstance(name, unicode):
51         name = name.encode('utf-8')
52     if not name:
53         return '_'
54     return _BAD.sub(lambda match: '_%02x' % ord(match.group(0)), name)
55
56 class Connection(_Connection, DBusProperties):
57
58     _optional_parameters = {}
59     _mandatory_parameters = {}
60
61     def __init__(self, proto, account, manager=None):
62         """
63         Parameters:
64         proto - the name of the protcol this conection should be handling.
65         account - a protocol-specific account name
66         manager - the name of the connection manager
67         """
68
69         if manager is None:
70             import warnings
71             warnings.warn('The manager parameter to Connection.__init__ '
72                           'should be supplied', DeprecationWarning)
73             manager = 'python'
74
75         clean_account = _escape_as_identifier(account)
76         bus_name = u'org.freedesktop.Telepathy.Connection.%s.%s.%s' % \
77                 (manager, proto, clean_account)
78         bus_name = dbus.service.BusName(bus_name, bus=dbus.SessionBus())
79
80         object_path = '/org/freedesktop/Telepathy/Connection/%s/%s/%s' % \
81                 (manager, proto, clean_account)
82         _Connection.__init__(self, bus_name, object_path)
83
84         # monitor clients dying so we can release handles
85         dbus.SessionBus().add_signal_receiver(self.name_owner_changed_callback,
86                                               'NameOwnerChanged',
87                                               'org.freedesktop.DBus',
88                                               'org.freedesktop.DBus',
89                                               '/org/freedesktop/DBus')
90
91         self._interfaces = set()
92
93         DBusProperties.__init__(self)
94         self._implement_property_get(CONN_INTERFACE,
95                 {'SelfHandle': lambda: dbus.UInt32(self.GetSelfHandle())})
96
97         self._proto = proto
98
99         self._status = CONNECTION_STATUS_DISCONNECTED
100
101         self._handles = weakref.WeakValueDictionary()
102         self._next_handle_id = 1
103         self._client_handles = {}
104
105         self._channels = set()
106         self._next_channel_id = 0
107
108     def check_parameters(self, parameters):
109         """
110         Uses the values of self._mandatory_parameters and
111         self._optional_parameters to validate and type check all of the
112         provided parameters, and check all mandatory parameters are present.
113         Sets defaults according to the defaults if the client has not
114         provided any.
115         """
116         for (parm, value) in parameters.iteritems():
117             if parm in self._mandatory_parameters.keys():
118                 sig = self._mandatory_parameters[parm]
119             elif parm in self._optional_parameters.keys():
120                 sig = self._optional_parameters[parm]
121             else:
122                 raise InvalidArgument('unknown parameter name %s' % parm)
123
124             # we currently support strings, (u)int16/32 and booleans
125             if sig == 's':
126                 if not isinstance(value, unicode):
127                     raise InvalidArgument('incorrect type to %s parameter, got %s, expected a string' % (parm, type(value)))
128             elif sig in 'iunq':
129                 if not isinstance(value, (int, long)):
130                     raise InvalidArgument('incorrect type to %s parameter, got %s, expected an int' % (parm, type(value)))
131             elif sig == 'b':
132                 if not isinstance(value, (bool, dbus.types.Boolean)):
133                     raise InvalidArgument('incorrect type to %s parameter, got %s, expected an boolean' % (parm, type(value)))
134             else:
135                 raise TypeError('unknown type signature %s in protocol parameters' % type)
136
137         for (parm, value) in self._parameter_defaults.iteritems():
138             if parm not in parameters:
139                 parameters[parm] = value
140
141         missing = set(self._mandatory_parameters.keys()).difference(parameters.keys())
142         if missing:
143             raise InvalidArgument('required parameters %s not given' % missing)
144
145     def check_connected(self):
146         if self._status != CONNECTION_STATUS_CONNECTED:
147             raise Disconnected('method cannot be called unless status is CONNECTION_STATUS_CONNECTED')
148
149     def check_handle(self, handle_type, handle):
150         if (handle_type, handle) not in self._handles:
151             print "Connection.check_handle", handle, handle_type, self._handles.keys()
152             print str(list( [ self._handles[x] for x in self._handles.keys() ] ) )
153             raise InvalidHandle('handle number %d not valid for type %d' %
154                 (handle, handle_type))
155
156     def check_handle_type(self, type):
157         if (type <= HANDLE_TYPE_NONE or type > LAST_HANDLE_TYPE):
158             raise InvalidArgument('handle type %s not known' % type)
159
160     def get_handle_id(self):
161         id = self._next_handle_id
162         self._next_handle_id += 1
163         return id
164
165     def add_client_handle(self, handle, sender):
166         if sender in self._client_handles:
167             self._client_handles[sender].add((handle.get_type(), handle))
168         else:
169             self._client_handles[sender] = set([(handle.get_type(), handle)])
170
171     def get_handle_by_id(self, handle_type, handle_id):
172         # Strip off dbus stuff so we can be consistent
173         handle_type, handle_id = int(handle_type), int(handle_id)
174
175         self.check_handle(handle_type, handle_id)
176         return self._handles[handle_type, handle_id]
177
178     def get_handle_by_name(self, handle_type, handle_name):
179         self.check_handle_type(handle_type)
180         handle = None
181
182         for candidate in self._handles.itervalues():
183             if candidate.get_name() == handle_name:
184                 handle = candidate
185                 break
186         else:
187             id = self.get_handle_id()
188             handle = Handle(id, handle_type, handle_name)
189             self._handles[handle_type, id] = handle
190
191         return handle
192
193     def name_owner_changed_callback(self, name, old_owner, new_owner):
194         # when name and old_owner are the same, and new_owner is
195         # blank, it is the client itself releasing its name... aka exiting
196         if (name == old_owner and new_owner == "" and name in self._client_handles):
197             print "deleting handles for", name
198             del self._client_handles[name]
199
200     def set_self_handle(self, handle):
201         self._self_handle = handle
202
203     def get_channel_path(self):
204         ret = '%s/channel%d' % (self._object_path, self._next_channel_id)
205         self._next_channel_id += 1
206         return ret
207
208     def add_channels(self, channels, signal=True):
209         """ add new channels and signal their creation"""
210         signal_channels = set()
211
212         for channel in channels:
213             if channel not in self._channels:
214                 self._channels.add(channel)
215                 signal_channels.add(channel)
216
217         if signal:
218             self.signal_new_channels(signal_channels)
219
220     def signal_new_channels(self, channels):
221         self.NewChannels([(channel._object_path, channel.get_props())
222             for channel in channels])
223
224         # Now NewChannel needs to be called for each new channel.
225         for channel in channels:
226             props = channel.get_props()
227
228             target_handle_type = props[CHANNEL_INTERFACE + '.TargetHandleType']
229             target_handle = props[CHANNEL_INTERFACE + '.TargetHandle']
230             suppress_handler = props[CHANNEL_INTERFACE + '.Requested']
231
232             self.NewChannel(channel._object_path, channel._type,
233                 target_handle_type, target_handle,
234                 suppress_handler)
235
236     def remove_channel(self, channel):
237         self._channels.remove(channel)
238         self.ChannelClosed(channel._object_path)
239
240     @dbus.service.method(CONN_INTERFACE, in_signature='', out_signature='as')
241     def GetInterfaces(self):
242         self.check_connected()
243         return self._interfaces
244
245     @dbus.service.method(CONN_INTERFACE, in_signature='', out_signature='s')
246     def GetProtocol(self):
247         return self._proto
248
249     @dbus.service.method(CONN_INTERFACE, in_signature='uau', out_signature='as')
250     def InspectHandles(self, handle_type, handles):
251         self.check_connected()
252         self.check_handle_type(handle_type)
253
254         for handle in handles:
255             self.check_handle(handle_type, handle)
256
257         ret = []
258         for handle in handles:
259             ret.append(self._handles[handle_type, handle].get_name())
260
261         return ret
262
263     @dbus.service.method(CONN_INTERFACE, in_signature='uas', out_signature='au', sender_keyword='sender')
264     def RequestHandles(self, handle_type, names, sender):
265         self.check_connected()
266         self.check_handle_type(handle_type)
267
268         ret = []
269         for name in names:
270             handle = self.get_handle_by_name(handle_type, name)
271             self.add_client_handle(handle, sender)
272             ret.append(handle.get_id())
273
274         return ret
275
276     @dbus.service.method(CONN_INTERFACE, in_signature='uau', out_signature='', sender_keyword='sender')
277     def HoldHandles(self, handle_type, handles, sender):
278         self.check_connected()
279         self.check_handle_type(handle_type)
280
281         for handle in handles:
282             self.check_handle(handle_type, handle)
283
284         for handle in handles:
285             hand = self._handles[handle_type, handle]
286             self.add_client_handle(hand, sender)
287
288     @dbus.service.method(CONN_INTERFACE, in_signature='uau', out_signature='', sender_keyword='sender')
289     def ReleaseHandles(self, handle_type, handles, sender):
290         self.check_connected()
291         self.check_handle_type(handle_type)
292
293         for handle in handles:
294             self.check_handle(handle_type, handle)
295             hand = self._handles[handle_type, handle]
296             if sender in self._client_handles:
297                 if (handle_type, hand) not in self._client_handles[sender]:
298                     raise NotAvailable('client is not holding handle %s of type %s' % (handle, handle_type))
299             else:
300                 raise NotAvailable('client does not hold any handles')
301
302         for handle in handles:
303             hand = self._handles[handle_type, handle]
304             self._client_handles[sender].remove((handle_type, hand))
305
306     @dbus.service.method(CONN_INTERFACE, in_signature='', out_signature='u')
307     def GetSelfHandle(self):
308         self.check_connected()
309         return self._self_handle
310
311     @dbus.service.signal(CONN_INTERFACE, signature='uu')
312     def StatusChanged(self, status, reason):
313         self._status = status
314
315     @dbus.service.method(CONN_INTERFACE, in_signature='', out_signature='u')
316     def GetStatus(self):
317         return self._status
318
319     @dbus.service.method(CONN_INTERFACE, in_signature='', out_signature='a(osuu)')
320     def ListChannels(self):
321         self.check_connected()
322         ret = []
323         for channel in self._channels:
324             chan = (channel._object_path, channel._type, channel._get_handle_type(), channel._handle)
325             ret.append(chan)
326         return ret
327
328
329 from telepathy._generated.Connection_Interface_Aliasing \
330         import ConnectionInterfaceAliasing
331
332
333 from telepathy._generated.Connection_Interface_Avatars \
334         import ConnectionInterfaceAvatars
335
336
337 from telepathy._generated.Connection_Interface_Capabilities \
338         import ConnectionInterfaceCapabilities \
339         as _ConnectionInterfaceCapabilities
340
341 class ConnectionInterfaceCapabilities(_ConnectionInterfaceCapabilities):
342     def __init__(self):
343         _ConnectionInterfaceCapabilities.__init__(self)
344         # { contact handle : { str channel type : [int, int] }}
345         # the first int is the generic caps, the second is the type-specific
346         self._caps = {}
347
348     @dbus.service.method(CONN_INTERFACE_CAPABILITIES, in_signature='au', out_signature='a(usuu)')
349     def GetCapabilities(self, handles):
350         ret = []
351         handle_type = HANDLE_TYPE_CONTACT
352         for handle in handles:
353             if (handle != 0 and (handle_type, handle) not in self._handles):
354                 raise InvalidHandle
355             elif handle in self._caps:
356                 types = self._caps[handle]
357                 for ctype, specs in types.items():
358                     ret.append([handle, ctype, specs[0], specs[1]])
359         return ret
360
361     @dbus.service.signal(CONN_INTERFACE_CAPABILITIES, signature='a(usuuuu)')
362     def CapabilitiesChanged(self, caps):
363         for handle, ctype, gen_old, gen_new, spec_old, spec_new in caps:
364             self._caps.setdefault(handle, {})[ctype] = [gen_new, spec_new]
365
366     @dbus.service.method(CONN_INTERFACE_CAPABILITIES,
367                          in_signature='a(su)as', out_signature='a(su)')
368     def AdvertiseCapabilities(self, add, remove):
369         my_caps = self._caps.setdefault(self._self_handle, {})
370
371         changed = {}
372         for ctype, spec_caps in add:
373             changed[ctype] = spec_caps
374         for ctype in remove:
375             changed[ctype] = None
376
377         caps = []
378         for ctype, spec_caps in changed.iteritems():
379             gen_old, spec_old = my_caps.get(ctype, (0, 0))
380             if spec_caps is None:
381                 # channel type no longer supported (provider has gone away)
382                 gen_new, spec_new = 0, 0
383             else:
384                 # channel type supports new capabilities
385                 gen_new, spec_new = gen_old, spec_old | spec_caps
386             if spec_old != spec_new or gen_old != gen_new:
387                 caps.append((self._self_handle, ctype, gen_old, gen_new,
388                             spec_old, spec_new))
389
390         self.CapabilitiesChanged(caps)
391
392         # return all my capabilities
393         return [(ctype, caps[1]) for ctype, caps in my_caps.iteritems()]
394
395 from telepathy._generated.Connection_Interface_Requests \
396         import ConnectionInterfaceRequests \
397         as _ConnectionInterfaceRequests
398
399 class ConnectionInterfaceRequests(
400     _ConnectionInterfaceRequests,
401     DBusProperties):
402
403     def __init__(self):
404         _ConnectionInterfaceRequests.__init__(self)
405         DBusProperties.__init__(self)
406
407         self._implement_property_get(CONNECTION_INTERFACE_REQUESTS,
408             {'Channels': lambda: dbus.Array(self._get_channels(),
409                 signature='(oa{sv})'),
410             'RequestableChannelClasses': lambda: dbus.Array(
411                 self._channel_manager.get_requestable_channel_classes(),
412                 signature='(a{sv}as)')})
413
414     def _get_channels(self):
415         return [(c._object_path, c.get_props()) for c in self._channels]
416
417     def _check_basic_properties(self, props):
418         # ChannelType must be present and must be a string.
419         if CHANNEL_INTERFACE + '.ChannelType' not in props or \
420                 not isinstance(props[CHANNEL_INTERFACE + '.ChannelType'],
421                     dbus.String):
422             raise InvalidArgument('ChannelType is required')
423
424         def check_valid_type_if_exists(prop, fun):
425             p = CHANNEL_INTERFACE + '.' + prop
426             if p in props and not fun(props[p]):
427                 raise InvalidArgument('Invalid %s' % prop)
428
429         # Allow TargetHandleType to be missing, but not to be otherwise broken.
430         check_valid_type_if_exists('TargetHandleType',
431             lambda p: p > 0 and p < (2**32)-1)
432
433         # Allow TargetType to be missing, but not to be otherwise broken.
434         check_valid_type_if_exists('TargetHandle',
435             lambda p: p > 0 and p < (2**32)-1)
436         if props.get(CHANNEL_INTERFACE + '.TargetHandle') == 0:
437             raise InvalidArgument("TargetHandle may not be 0")
438
439         # Allow TargetID to be missing, but not to be otherwise broken.
440         check_valid_type_if_exists('TargetID',
441             lambda p: isinstance(p, dbus.String))
442
443         # Disallow InitiatorHandle, InitiatorID and Requested.
444         check_valid_type_if_exists('InitiatorHandle', lambda p: False)
445         check_valid_type_if_exists('InitiatorID', lambda p: False)
446         check_valid_type_if_exists('Requested', lambda p: False)
447
448         type = props[CHANNEL_INTERFACE + '.ChannelType']
449         handle_type = props.get(CHANNEL_INTERFACE + '.TargetHandleType',
450                 HANDLE_TYPE_NONE)
451         handle = props.get(CHANNEL_INTERFACE + '.TargetHandle', 0)
452
453         return (type, handle_type, handle)
454
455     def _validate_handle(self, props):
456         target_handle_type = props.get(CHANNEL_INTERFACE + '.TargetHandleType',
457             HANDLE_TYPE_NONE)
458         target_handle = props.get(CHANNEL_INTERFACE + '.TargetHandle', None)
459         target_id = props.get(CHANNEL_INTERFACE + '.TargetID', None)
460
461         # Handle type 0 cannot have a handle.
462         if target_handle_type == HANDLE_TYPE_NONE and target_handle != None:
463             raise InvalidArgument('When TargetHandleType is NONE, ' +
464                 'TargetHandle must be omitted')
465
466         # Handle type 0 cannot have a TargetID.
467         if target_handle_type == HANDLE_TYPE_NONE and target_id != None:
468             raise InvalidArgument('When TargetHandleType is NONE, TargetID ' +
469                 'must be omitted')
470
471         if target_handle_type != HANDLE_TYPE_NONE:
472             if target_handle == None and target_id == None:
473                 raise InvalidArgument('When TargetHandleType is not NONE, ' +
474                     'either TargetHandle or TargetID must also be given')
475
476             if target_handle != None and target_id != None:
477                 raise InvalidArgument('TargetHandle and TargetID must not ' +
478                     'both be given')
479
480             self.check_handle_type(target_handle_type)
481
482
483     def _alter_properties(self, props):
484         target_handle_type = props.get(CHANNEL_INTERFACE + '.TargetHandleType',
485             HANDLE_TYPE_NONE)
486         target_handle = props.get(CHANNEL_INTERFACE + '.TargetHandle', None)
487         target_id = props.get(CHANNEL_INTERFACE + '.TargetID', None)
488         # Note: what the spec calls a handle, we call an ID
489         # What the spec calls an ID, we call a name
490         # What we call a handle is a handle object
491
492         altered_properties = props.copy()
493
494         if target_handle_type != HANDLE_TYPE_NONE:
495             if target_handle is None:
496                 handle = self.get_handle_by_name(target_handle_type, target_id)
497                 self.add_client_handle(handle, None)
498                 target_handle = handle.get_id()
499                 altered_properties[CHANNEL_INTERFACE + '.TargetHandle'] = \
500                     target_handle
501             else:
502                 # Check the supplied TargetHandle is valid
503                 self.check_handle(target_handle_type, target_handle)
504
505                 target_id = self._handles[target_handle_type,\
506                                             target_handle].get_name()
507                 altered_properties[CHANNEL_INTERFACE + '.TargetID'] = \
508                     target_id
509
510         altered_properties[CHANNEL_INTERFACE + '.Requested'] = True
511
512         return altered_properties
513
514     @dbus.service.method(CONNECTION_INTERFACE_REQUESTS,
515         in_signature='a{sv}', out_signature='oa{sv}',
516         async_callbacks=('_success', '_error'))
517     def CreateChannel(self, request, _success, _error):
518         type, handle_type, handle = self._check_basic_properties(request)
519         self._validate_handle(request)
520         props = self._alter_properties(request)
521
522         channel = self._channel_manager.create_channel_for_props(props, signal=False)
523
524         # Remove mutable properties
525         todel = []
526         for prop in props:
527             iface, name = prop.rsplit('.', 1) # a bit of a hack
528             if name in channel._immutable_properties:
529                 if channel._immutable_properties[name] != iface:
530                     todel.append(prop)
531             else:
532                 todel.append(prop)
533
534         for p in todel:
535             del props[p]
536
537         _success(channel._object_path, props)
538
539         # CreateChannel MUST return *before* NewChannels is emitted.
540         self.signal_new_channels([channel])
541
542     @dbus.service.method(CONNECTION_INTERFACE_REQUESTS,
543         in_signature='a{sv}', out_signature='boa{sv}',
544         async_callbacks=('_success', '_error'))
545     def EnsureChannel(self, request, _success, _error):
546         type, handle_type, handle = self._check_basic_properties(request)
547         self._validate_handle(request)
548         props = self._alter_properties(request)
549
550         yours = not self._channel_manager.channel_exists(props)
551
552         channel = self._channel_manager.channel_for_props(props, signal=False)
553
554         _success(yours, channel._object_path, props)
555
556         self.signal_new_channels([channel])
557
558 from telepathy._generated.Connection_Interface_Presence \
559         import ConnectionInterfacePresence
560
561 from telepathy._generated.Connection_Interface_Simple_Presence \
562         import ConnectionInterfaceSimplePresence
563
564 from telepathy._generated.Connection_Interface_Contacts \
565         import ConnectionInterfaceContacts