diff options
Diffstat (limited to 'sugargame/mesh.py')
-rw-r--r-- | sugargame/mesh.py | 435 |
1 files changed, 435 insertions, 0 deletions
diff --git a/sugargame/mesh.py b/sugargame/mesh.py new file mode 100644 index 0000000..1fbf7ca --- /dev/null +++ b/sugargame/mesh.py @@ -0,0 +1,435 @@ +import sugargame + +from sugar3.presence.tubeconn import TubeConnection +from dbus.gobject_service import ExportedGObject +from dbus.service import method, signal + +import telepathy +import sugar3.presence.presenceservice +import pygame.event as PEvent + + +class OfflineError(Exception): + """Raised when we cannot complete an operation due to being offline""" + +DBUS_IFACE = "org.laptop.games.pygame" +DBUS_PATH = "/org/laptop/games/pygame" +DBUS_SERVICE = None + + +### NEW PYGAME EVENTS ### + +CONNECT = sugargame.CONNECT +PARTICIPANT_ADD = sugargame.PARTICIPANT_ADD +PARTICIPANT_REMOVE = sugargame.PARTICIPANT_REMOVE +MESSAGE_UNI = sugargame.MESSAGE_UNI +MESSAGE_MULTI = sugargame.MESSAGE_MULTI + + +# Private objects for useful purposes! +pygametubes = [] +text_chan, tubes_chan = (None, None) +conn = None +initiating = False +joining = False + +connect_callback = None + + +def is_initiating(): + '''A version of is_initiator that's a bit less goofy, and can be used + before the Tube comes up.''' + global initiating + return initiating + + +def is_joining(): + '''Returns True if the activity was started up by means of the + Neighbourhood mesh view.''' + global joining + return joining + + +def set_connect_callback(cb): + '''Just the same as the Pygame event loop can listen for CONNECT, + this is just an ugly callback that the glib side can use to be aware + of when the Tube is ready.''' + global connect_callback + connect_callback = cb + + +def activity_shared(activity): + '''Called when the user clicks Share.''' + + global initiating + initiating = True + + _setup(activity) + + channel = tubes_chan[telepathy.CHANNEL_TYPE_TUBES] + if hasattr(channel, 'OfferDBusTube'): + channel.OfferDBusTube(DBUS_SERVICE, {}) + else: + channel.OfferTube(telepathy.TUBE_TYPE_DBUS, DBUS_SERVICE, {}) + + global connect_callback + if connect_callback is not None: + connect_callback() + + +def activity_joined(activity): + '''Called at the startup of our Activity, + when the user started it via Neighborhood intending to join + an existing activity.''' + + global initiating + global joining + initiating = False + joining = True + + _setup(activity) + + tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes( + reply_handler=_list_tubes_reply_cb, + error_handler=_list_tubes_error_cb) + + global connect_callback + if connect_callback is not None: + connect_callback() + + +def _getConn(activity): + global conn + if conn: + return conn + else: + if hasattr(activity.shared_activity, 'telepathy_conn'): + conn = activity.shared_activity.telepathy_conn + else: + pservice = _get_presence_service() + name, path = pservice.get_preferred_connection() + conn = telepathy.client.Connection(name, path) + return conn + + +def _setup(activity): + '''Determines text and tube channels for the current Activity. If no tube +channel present, creates one. Updates text_chan and tubes_chan. + +setup(sugar.activity.Activity, telepathy.client.Connection)''' + global text_chan, tubes_chan, DBUS_SERVICE + if not DBUS_SERVICE: + DBUS_SERVICE = activity.get_bundle_id() + if not activity.get_shared(): + raise "Failure" + + if hasattr(activity.shared_activity, 'telepathy_tubes_chan'): + _getConn(activity) + conn = activity.shared_activity.telepathy_conn + tubes_chan = activity.shared_activity.telepathy_tubes_chan + text_chan = activity.shared_activity.telepathy_text_chan + else: + bus_name, conn_path, channel_paths = \ + activity.shared_activity.get_channels() + _getConn(activity) + + # Work out what our room is called and whether we have Tubes already + room = None + tubes_chan = None + text_chan = None + for channel_path in channel_paths: + channel = telepathy.client.Channel(bus_name, channel_path) + htype, handle = channel.GetHandle() + if htype == telepathy.HANDLE_TYPE_ROOM: + room = handle + ctype = channel.GetChannelType() + if ctype == telepathy.CHANNEL_TYPE_TUBES: + tubes_chan = channel + elif ctype == telepathy.CHANNEL_TYPE_TEXT: + text_chan = channel + + if room is None: + raise "Failure" + + if text_chan is None: + raise "Failure" + + # Make sure we have a Tubes channel - PS doesn't yet provide one + if tubes_chan is None: + tubes_chan = conn.request_channel(telepathy.CHANNEL_TYPE_TUBES, + telepathy.HANDLE_TYPE_ROOM, room, True) + + tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal('NewTube', + new_tube_cb) + + return (text_chan, tubes_chan) + + +def new_tube_cb(id, initiator, type, service, params, state): + if (type == telepathy.TUBE_TYPE_DBUS and service == DBUS_SERVICE): + if state == telepathy.TUBE_STATE_LOCAL_PENDING: + channel = tubes_chan[telepathy.CHANNEL_TYPE_TUBES] + if hasattr(channel, 'AcceptDBusTube'): + channel.AcceptDBusTube(id) + else: + channel.AcceptTube(id) + + tube_conn = TubeConnection(conn, + tubes_chan[telepathy.CHANNEL_TYPE_TUBES], + id, group_iface=text_chan[telepathy.CHANNEL_INTERFACE_GROUP]) + + global pygametubes, initiating + pygametubes.append(PygameTube(tube_conn, initiating, len(pygametubes))) + + +def _list_tubes_reply_cb(tubes): + for tube_info in tubes: + new_tube_cb(*tube_info) + + +def _list_tubes_error_cb(e): + pass + + +def lookup_buddy(dbus_handle, callback, errback=None): + """Do a lookup on the buddy information, callback with the information + + Calls callback(buddy) with the result of the lookup, or errback(error) with + a dbus description of the error in the lookup process. + + returns None + """ + + cs_handle = instance().tube.bus_name_to_handle[dbus_handle] + group = text_chan[telepathy.CHANNEL_INTERFACE_GROUP] + if not errback: + def errback(error): + pass + + def with_my_csh(my_csh): + + def _withHandle(handle): + """process the results of the handle values""" + # XXX: we're assuming that we have Buddy objects for all contacts - + # this might break when the server becomes scalable. + pservice = _get_presence_service() + name, path = pservice.get_preferred_connection() + callback(pservice.get_buddy_by_telepathy_handle(name, path, handle)) + if my_csh == cs_handle: + conn.GetSelfHandle(reply_handler=_withHandle, + error_handler=errback) + elif group.GetGroupFlags() & \ + telepathy.CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES: + handle = group.GetHandleOwners([cs_handle])[0] + _withHandle(handle) + else: + handle = cs_handle + _withHandle(handle) + group.GetSelfHandle(reply_handler=with_my_csh, error_handler=errback) + + +def get_buddy(dbus_handle): + """DEPRECATED: Get a Buddy from a handle + + THIS API WAS NOT THREAD SAFE! It has been removed to avoid + extremely hard-to-debug failures in activities. Use lookup_buddy + instead! + + Code that read: + + get_buddy(handle) + doSomething(handle, buddy) + doSomethingElse(buddy) + + Translates to: + + def withBuddy(buddy): + doSomething(handle, buddy) + doSomethingElse(buddy) + lookup_buddy(handle, callback=withBuddy) + """ + raise RuntimeError( + """get_buddy is not thread safe and will crash your activity (hard). + Use lookup_buddy.""" + ) + + +def _get_presence_service(): + """Attempt to retrieve the presence service (check for offline condition) + + The presence service, when offline, has no preferred connection type, + so we check that before returning the object... + """ + + try: + pservice = sugar3.presence.presenceservice.get_instance() + try: + name, path = pservice.get_preferred_connection() + except (TypeError, ValueError): + raise OfflineError("""Unable to retrieve buddy information, + currently offline""") + else: + return pservice + except Exception: + pass + + +def instance(idx=0): + return pygametubes[idx] + + +class PygameTube(ExportedGObject): + '''The object whose instance is shared across D-bus + + Call instance() to get the instance of this object for + your activity service. + Its 'tube' property contains the underlying D-bus Connection. + ''' + def __init__(self, tube, is_initiator, tube_id): + super(PygameTube, self).__init__(tube, DBUS_PATH) + self.tube = tube + self.is_initiator = is_initiator + self.entered = False + self.ordered_bus_names = [] + PEvent.post(PEvent.Event(CONNECT, id=tube_id)) + + if not self.is_initiator: + self.tube.add_signal_receiver(self.new_participant_cb, + 'NewParticipants', DBUS_IFACE, path=DBUS_PATH) + self.tube.watch_participants(self.participant_change_cb) + self.tube.add_signal_receiver(self.broadcast_cb, 'Broadcast', + DBUS_IFACE, path=DBUS_PATH, sender_keyword='sender') + + def participant_change_cb(self, added, removed): + for handle, bus_name in added: + dbus_handle = self.tube.participants[handle] + self.ordered_bus_names.append(dbus_handle) + PEvent.post(PEvent.Event(PARTICIPANT_ADD, handle=dbus_handle)) + + for handle in removed: + dbus_handle = self.tube.participants[handle] + self.ordered_bus_names.remove(dbus_handle) + PEvent.post(PEvent.Event(PARTICIPANT_REMOVE, handle=dbus_handle)) + + if self.is_initiator: + if not self.entered: + # Initiator will broadcast a new ordered_bus_names each time + # a participant joins. + self.ordered_bus_names = [self.tube.get_unique_name()] + self.NewParticipants(self.ordered_bus_names) + + self.entered = True + + @signal(dbus_interface=DBUS_IFACE, signature='as') + def NewParticipants(self, ordered_bus_names): + '''This is the NewParticipants signal, + sent when the authoritative list of ordered_bus_names changes.''' + pass + + @signal(dbus_interface=DBUS_IFACE, signature='s') + def Broadcast(self, content): + '''This is the Broadcast signal; + it sends a message to all other activity participants.''' + pass + + @method(dbus_interface=DBUS_IFACE, in_signature='s', out_signature='', + sender_keyword='sender') + def Tell(self, content, sender=None): + '''This is the targeted-message interface; + called when a message is received that was sent directly to me.''' + PEvent.post(PEvent.Event(MESSAGE_UNI, handle=sender, content=content)) + + def broadcast_cb(self, content, sender=None): + '''This is the Broadcast callback, fired when someone sends a Broadcast + signal along the bus.''' + PEvent.post(PEvent.Event(MESSAGE_MULTI, handle=sender, content=content)) + + def new_participant_cb(self, new_bus_names): + '''This is the NewParticipants callback, + fired when someone joins or leaves.''' + if self.ordered_bus_names != new_bus_names: + self.ordered_bus_names = new_bus_names + + +def send_to(handle, content=""): + '''Sends the given message to the given buddy identified by handle.''' + remote_proxy = dbus_get_object(handle, DBUS_PATH) + remote_proxy.Tell(content, reply_handler=dbus_msg, error_handler=dbus_err) + + +def dbus_msg(): + pass + + +def dbus_err(e): + pass + + +def broadcast(content=""): + '''Sends the given message to all participants.''' + instance().Broadcast(content) + + +def my_handle(): + '''Returns the handle of this user + + Note, you can get a DBusException from this if you have + not yet got a unique ID assigned by the bus. You may need + to delay calling until you are sure you are connected. + ''' + return instance().tube.get_unique_name() + + +def is_initiator(): + '''Returns the handle of this user.''' + return instance().is_initiator + + +def get_participants(): + '''Returns the list of active participants, in order of arrival. + List is maintained by the activity creator; + if that person leaves it may not stay in sync.''' + + try: + return instance().ordered_bus_names[:] + except IndexError: + return [] # no participants yet, as we don't yet have a connection + + +def dbus_get_object(handle, path, warning=True): + '''Get a D-bus object from another participant + + Note: this *must* be called *only* from the GTK mainloop, calling + it from Pygame will cause crashes! If you are *sure* you only ever + want to call methods on this proxy from GTK, you can use + warning=False to silence the warning log message. + ''' + return instance().tube.get_object(handle, path) + + +def get_object(handle, path): + '''Get a D-BUS proxy object from another participant for use in Pygame + + This is how you can communicate with other participants using + arbitrary D-bus objects without having to manage the participants + yourself. You can use the returned proxy's methods from Pygame, + with your callbacks occuring in the Pygame thread, rather than + in the DBUS/GTK event loop. + + Simply define a D-bus class with an interface and path that you + choose; when you want a reference to the corresponding remote + object on a participant, call this method. + + returns an sugargame.dbusproxy.DBUSProxy() object wrapping + the DBUSProxy object. + + The dbus_get_object_proxy name is deprecated + ''' + from sugargame import dbusproxy + return dbusproxy.DBUSProxy( + instance().tube.get_object(handle, path), + tube=instance().tube, + path=path + ) + +dbus_get_object_proxy = get_object |