Updating from my tor-fixes of python-telepathy
[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):
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         self.check_handle(handle_type, handle_id)
173         return self._handles[handle_type, handle_id]
174
175     def get_handle_by_name(self, handle_type, handle_name):
176         self.check_handle_type(handle_type)
177         handle = None
178
179         for candidate in self._handles.itervalues():
180             if candidate.get_name() == handle_name:
181                 handle = candidate
182                 break
183         else:
184             id = self.get_handle_id()
185             handle = Handle(id, handle_type, handle_name)
186             self._handles[handle_type, id] = handle
187
188         return handle
189
190     def name_owner_changed_callback(self, name, old_owner, new_owner):
191         # when name and old_owner are the same, and new_owner is
192         # blank, it is the client itself releasing its name... aka exiting
193         if (name == old_owner and new_owner == "" and name in self._client_handles):
194             print "deleting handles for", name
195             del self._client_handles[name]
196
197     def set_self_handle(self, handle):
198         self._self_handle = handle
199
200     def get_channel_path(self):
201         ret = '%s/channel%d' % (self._object_path, self._next_channel_id)
202         self._next_channel_id += 1
203         return ret
204
205     def add_channels(self, channels, signal=True):
206         """ add new channels and signal their creation"""
207         signal_channels = set()
208
209         for channel in channels:
210             if channel not in self._channels:
211                 self._channels.add(channel)
212                 signal_channels.add(channel)
213
214         if signal:
215             self.signal_new_channels(signal_channels)
216
217     def signal_new_channels(self, channels):
218         self.NewChannels([(channel._object_path, channel.get_props())
219             for channel in channels])
220
221         # Now NewChannel needs to be called for each new channel.
222         for channel in channels:
223             props = channel.get_props()
224
225             target_handle_type = props[CHANNEL_INTERFACE + '.TargetHandleType']
226             target_handle = props[CHANNEL_INTERFACE + '.TargetHandle']
227             suppress_handler = props[CHANNEL_INTERFACE + '.Requested']
228
229             self.NewChannel(channel._object_path, channel._type,
230                 target_handle_type, target_handle,
231                 suppress_handler)
232
233     def remove_channel(self, channel):
234         self._channels.remove(channel)
235         self.ChannelClosed(channel._object_path)
236
237     @dbus.service.method(CONN_INTERFACE, in_signature='', out_signature='as')
238     def GetInterfaces(self):
239         self.check_connected()
240         return self._interfaces
241
242     @dbus.service.method(CONN_INTERFACE, in_signature='', out_signature='s')
243     def GetProtocol(self):
244         return self._proto
245
246     @dbus.service.method(CONN_INTERFACE, in_signature='uau', out_signature='as')
247     def InspectHandles(self, handle_type, handles):
248         self.check_connected()
249         self.check_handle_type(handle_type)
250
251         for handle in handles:
252             self.check_handle(handle_type, handle)
253
254         ret = []
255         for handle in handles:
256             ret.append(self._handles[handle_type, handle].get_name())
257
258         return ret
259
260     @dbus.service.method(CONN_INTERFACE, in_signature='uas', out_signature='au', sender_keyword='sender')
261     def RequestHandles(self, handle_type, names, sender):
262         self.check_connected()
263         self.check_handle_type(handle_type)
264
265         ret = []
266         for name in names:
267             handle = self.get_handle_by_name(handle_type, name)
268             self.add_client_handle(handle, sender)
269             ret.append(handle.get_id())
270
271         return ret
272
273     @dbus.service.method(CONN_INTERFACE, in_signature='uau', out_signature='', sender_keyword='sender')
274     def HoldHandles(self, handle_type, handles, sender):
275         self.check_connected()
276         self.check_handle_type(handle_type)
277
278         for handle in handles:
279             self.check_handle(handle_type, handle)
280
281         for handle in handles:
282             hand = self._handles[handle_type, handle]
283             self.add_client_handle(hand, sender)
284
285     @dbus.service.method(CONN_INTERFACE, in_signature='uau', out_signature='', sender_keyword='sender')
286     def ReleaseHandles(self, handle_type, handles, sender):
287         self.check_connected()
288         self.check_handle_type(handle_type)
289
290         for handle in handles:
291             self.check_handle(handle_type, handle)
292             hand = self._handles[handle_type, handle]
293             if sender in self._client_handles:
294                 if (handle_type, hand) not in self._client_handles[sender]:
295                     raise NotAvailable('client is not holding handle %s of type %s' % (handle, handle_type))
296             else:
297                 raise NotAvailable('client does not hold any handles')
298
299         for handle in handles:
300             hand = self._handles[handle_type, handle]
301             self._client_handles[sender].remove((handle_type, hand))
302
303     @dbus.service.method(CONN_INTERFACE, in_signature='', out_signature='u')
304     def GetSelfHandle(self):
305         self.check_connected()
306         return self._self_handle
307
308     @dbus.service.signal(CONN_INTERFACE, signature='uu')
309     def StatusChanged(self, status, reason):
310         self._status = status
311
312     @dbus.service.method(CONN_INTERFACE, in_signature='', out_signature='u')
313     def GetStatus(self):
314         return self._status
315
316     @dbus.service.method(CONN_INTERFACE, in_signature='', out_signature='a(osuu)')
317     def ListChannels(self):
318         self.check_connected()
319         ret = []
320         for channel in self._channels:
321             chan = (channel._object_path, channel._type, channel._get_handle_type(), channel._handle)
322             ret.append(chan)
323         return ret
324
325
326 from telepathy._generated.Connection_Interface_Aliasing \
327         import ConnectionInterfaceAliasing
328
329
330 from telepathy._generated.Connection_Interface_Avatars \
331         import ConnectionInterfaceAvatars
332
333
334 from telepathy._generated.Connection_Interface_Capabilities \
335         import ConnectionInterfaceCapabilities \
336         as _ConnectionInterfaceCapabilities
337
338 class ConnectionInterfaceCapabilities(_ConnectionInterfaceCapabilities):
339     def __init__(self):
340         _ConnectionInterfaceCapabilities.__init__(self)
341         # { contact handle : { str channel type : [int, int] }}
342         # the first int is the generic caps, the second is the type-specific
343         self._caps = {}
344
345     @dbus.service.method(CONN_INTERFACE_CAPABILITIES, in_signature='au', out_signature='a(usuu)')
346     def GetCapabilities(self, handles):
347         ret = []
348         handle_type = HANDLE_TYPE_CONTACT
349         for handle in handles:
350             if (handle != 0 and (handle_type, handle) not in self._handles):
351                 raise InvalidHandle
352             elif handle in self._caps:
353                 types = self._caps[handle]
354                 for ctype, specs in types.items():
355                     ret.append([handle, ctype, specs[0], specs[1]])
356         return ret
357
358     @dbus.service.signal(CONN_INTERFACE_CAPABILITIES, signature='a(usuuuu)')
359     def CapabilitiesChanged(self, caps):
360         for handle, ctype, gen_old, gen_new, spec_old, spec_new in caps:
361             self._caps.setdefault(handle, {})[ctype] = [gen_new, spec_new]
362
363     @dbus.service.method(CONN_INTERFACE_CAPABILITIES,
364                          in_signature='a(su)as', out_signature='a(su)')
365     def AdvertiseCapabilities(self, add, remove):
366         my_caps = self._caps.setdefault(self._self_handle, {})
367
368         changed = {}
369         for ctype, spec_caps in add:
370             changed[ctype] = spec_caps
371         for ctype in remove:
372             changed[ctype] = None
373
374         caps = []
375         for ctype, spec_caps in changed.iteritems():
376             gen_old, spec_old = my_caps.get(ctype, (0, 0))
377             if spec_caps is None:
378                 # channel type no longer supported (provider has gone away)
379                 gen_new, spec_new = 0, 0
380             else:
381                 # channel type supports new capabilities
382                 gen_new, spec_new = gen_old, spec_old | spec_caps
383             if spec_old != spec_new or gen_old != gen_new:
384                 caps.append((self._self_handle, ctype, gen_old, gen_new,
385                             spec_old, spec_new))
386
387         self.CapabilitiesChanged(caps)
388
389         # return all my capabilities
390         return [(ctype, caps[1]) for ctype, caps in my_caps.iteritems()]
391
392 from telepathy._generated.Connection_Interface_Requests \
393         import ConnectionInterfaceRequests \
394         as _ConnectionInterfaceRequests
395
396 class ConnectionInterfaceRequests(
397     _ConnectionInterfaceRequests,
398     DBusProperties):
399
400     def __init__(self):
401         _ConnectionInterfaceRequests.__init__(self)
402         DBusProperties.__init__(self)
403
404         self._implement_property_get(CONNECTION_INTERFACE_REQUESTS,
405             {'Channels': lambda: dbus.Array(self._get_channels(),
406                 signature='(oa{sv})'),
407             'RequestableChannelClasses': lambda: dbus.Array(
408                 self._channel_manager.get_requestable_channel_classes(),
409                 signature='(a{sv}as)')})
410
411     def _get_channels(self):
412         return [(c._object_path, c.get_props()) for c in self._channels]
413
414     def _check_basic_properties(self, props):
415         # ChannelType must be present and must be a string.
416         if CHANNEL_INTERFACE + '.ChannelType' not in props or \
417                 not isinstance(props[CHANNEL_INTERFACE + '.ChannelType'],
418                     dbus.String):
419             raise InvalidArgument('ChannelType is required')
420
421         def check_valid_type_if_exists(prop, fun):
422             p = CHANNEL_INTERFACE + '.' + prop
423             if p in props and not fun(props[p]):
424                 raise InvalidArgument('Invalid %s' % prop)
425
426         # Allow TargetHandleType to be missing, but not to be otherwise broken.
427         check_valid_type_if_exists('TargetHandleType',
428             lambda p: p > 0 and p < (2**32)-1)
429
430         # Allow TargetType to be missing, but not to be otherwise broken.
431         check_valid_type_if_exists('TargetHandle',
432             lambda p: p > 0 and p < (2**32)-1)
433         if props.get(CHANNEL_INTERFACE + '.TargetHandle') == 0:
434             raise InvalidArgument("TargetHandle may not be 0")
435
436         # Allow TargetID to be missing, but not to be otherwise broken.
437         check_valid_type_if_exists('TargetID',
438             lambda p: isinstance(p, dbus.String))
439
440         # Disallow InitiatorHandle, InitiatorID and Requested.
441         check_valid_type_if_exists('InitiatorHandle', lambda p: False)
442         check_valid_type_if_exists('InitiatorID', lambda p: False)
443         check_valid_type_if_exists('Requested', lambda p: False)
444
445         type = props[CHANNEL_INTERFACE + '.ChannelType']
446         handle_type = props.get(CHANNEL_INTERFACE + '.TargetHandleType',
447                 HANDLE_TYPE_NONE)
448         handle = props.get(CHANNEL_INTERFACE + '.TargetHandle', 0)
449
450         return (type, handle_type, handle)
451
452     def _validate_handle(self, props):
453         target_handle_type = props.get(CHANNEL_INTERFACE + '.TargetHandleType',
454             HANDLE_TYPE_NONE)
455         target_handle = props.get(CHANNEL_INTERFACE + '.TargetHandle', None)
456         target_id = props.get(CHANNEL_INTERFACE + '.TargetID', None)
457
458         # Handle type 0 cannot have a handle.
459         if target_handle_type == HANDLE_TYPE_NONE and target_handle != None:
460             raise InvalidArgument('When TargetHandleType is NONE, ' +
461                 'TargetHandle must be omitted')
462
463         # Handle type 0 cannot have a TargetID.
464         if target_handle_type == HANDLE_TYPE_NONE and target_id != None:
465             raise InvalidArgument('When TargetHandleType is NONE, TargetID ' +
466                 'must be omitted')
467
468         if target_handle_type != HANDLE_TYPE_NONE:
469             if target_handle == None and target_id == None:
470                 raise InvalidArgument('When TargetHandleType is not NONE, ' +
471                     'either TargetHandle or TargetID must also be given')
472
473             if target_handle != None and target_id != None:
474                 raise InvalidArgument('TargetHandle and TargetID must not ' +
475                     'both be given')
476
477             self.check_handle_type(target_handle_type)
478
479
480     def _alter_properties(self, props):
481         target_handle_type = props.get(CHANNEL_INTERFACE + '.TargetHandleType',
482             HANDLE_TYPE_NONE)
483         target_handle = props.get(CHANNEL_INTERFACE + '.TargetHandle', None)
484         target_id = props.get(CHANNEL_INTERFACE + '.TargetID', None)
485         # Note: what the spec calls a handle, we call an ID
486         # What the spec calls an ID, we call a name
487         # What we call a handle is a handle object
488
489         altered_properties = props.copy()
490
491         if target_handle_type != HANDLE_TYPE_NONE:
492             if target_handle == None:
493                 target_handle = self.get_handle_by_name(target_handle_type, target_id).get_id()
494                 altered_properties[CHANNEL_INTERFACE + '.TargetHandle'] = \
495                     target_handle
496             else:
497                 # Check the supplied TargetHandle is valid
498                 self.check_handle(target_handle_type, target_handle)
499
500                 target_id = self._handles[target_handle_type,\
501                                             target_handle].get_name()
502                 altered_properties[CHANNEL_INTERFACE + '.TargetID'] = \
503                     target_id
504
505         altered_properties[CHANNEL_INTERFACE + '.Requested'] = True
506
507         return altered_properties
508
509     @dbus.service.method(CONNECTION_INTERFACE_REQUESTS,
510         in_signature='a{sv}', out_signature='oa{sv}',
511         async_callbacks=('_success', '_error'))
512     def CreateChannel(self, request, _success, _error):
513         type, handle_type, handle = self._check_basic_properties(request)
514         self._validate_handle(request)
515         props = self._alter_properties(request)
516
517         channel = self._channel_manager.channel_for_props(props, signal=False)
518
519         # Remove mutable properties
520         todel = []
521         for prop in props:
522             iface, name = prop.rsplit('.', 1) # a bit of a hack
523             if name in channel._immutable_properties:
524                 if channel._immutable_properties[name] != iface:
525                     todel.append(prop)
526             else:
527                 todel.append(prop)
528
529         for p in todel:
530             del props[p]
531
532         _success(channel._object_path, props)
533
534         # CreateChannel MUST return *before* NewChannels is emitted.
535         self.signal_new_channels([channel])
536
537     @dbus.service.method(CONNECTION_INTERFACE_REQUESTS,
538         in_signature='a{sv}', out_signature='boa{sv}',
539         async_callbacks=('_success', '_error'))
540     def EnsureChannel(self, request, _success, _error):
541         type, handle_type, handle = self._check_basic_properties(request)
542         self._validate_handle(request)
543         props = self._alter_properties(request)
544
545         yours = not self._channel_manager.channel_exists(props)
546
547         channel = self._channel_manager.channel_for_props(props, signal=False)
548
549         _success(yours, channel._object_path, props)
550
551         self.signal_new_channels([channel])
552
553 from telepathy._generated.Connection_Interface_Presence \
554         import ConnectionInterfacePresence
555
556 from telepathy._generated.Connection_Interface_Simple_Presence \
557         import ConnectionInterfaceSimplePresence
558
559 from telepathy._generated.Connection_Interface_Contacts \
560         import ConnectionInterfaceContacts