diff --git a/activity.py b/activity.py index 79c8455..45b92b1 100644 --- a/activity.py +++ b/activity.py @@ -13,13 +13,12 @@ import os import shutil import time -from ConfigParser import ConfigParser +from configparser import ConfigParser import json from gettext import gettext as _ import gi gi.require_version('Gtk', '3.0') -gi.require_version('GConf', '2.0') gi.require_version('Vte', '2.91') gi.require_version('Wnck', '3.0') @@ -42,17 +41,10 @@ from sugar3 import profile from sugar3.datastore import datastore -import telepathy -import dbus -from dbus.service import signal -from dbus.gobject_service import ExportedGObject -from sugar3.presence import presenceservice + from sugar3.graphics.objectchooser import ObjectChooser -try: - from sugar3.presence.wrapper import CollabWrapper -except ImportError: - from textchannelwrapper import CollabWrapper +from collabwrapper import CollabWrapper from reflectwindow import ReflectWindow @@ -83,12 +75,9 @@ class ReflectActivity(activity.Activity): def __init__(self, handle): ''' Initialize the toolbar ''' - try: - super(ReflectActivity, self).__init__(handle) - except dbus.exceptions.DBusException as e: - _logger.error(str(e)) + super(ReflectActivity, self).__init__(handle) - logging.error('setting reflection data to []') + logging.debug('setting reflection data to []') self.reflection_data = [] self.connect('realize', self.__realize_cb) @@ -125,29 +114,34 @@ def __init__(self, handle): self._fixed = None self.initiating = True - if self.shared_activity: - # We're joining - if not self.get_shared(): - self.initiating = False - - self.busy_cursor() - share_icon = Icon(icon_name='zoom-neighborhood') - self._joined_alert = Alert() - self._joined_alert.props.icon = share_icon - self._joined_alert.props.title = _('Please wait') - self._joined_alert.props.msg = _('Starting connection...') - self.add_alert(self._joined_alert) - - # Wait for joined signal - self.connect("joined", self._joined_cb) + self._setup_dispatch_table() self._open_reflect_windows() - self._setup_presence_service() + self.connect('shared', self._shared_cb) + + self.collab = CollabWrapper(self) + + self.collab.connect('message', self._message_cb) + + self.collab.connect('joined', self._joined_cb) + + def on_buddy_joined_cb(collab, buddy, msg): + logging.debug('on_buddy_joined_cb buddy %r' % (buddy.props.nick)) + + self.collab.connect('buddy_joined', on_buddy_joined_cb, + 'buddy_joined') + + def on_buddy_left_cb(collab, buddy, msg): + logging.debug('on_buddy_left_cb buddy %r' % (buddy.props.nick)) + + self.collab.connect('buddy_left', on_buddy_left_cb, 'buddy_left') + + self.collab.setup() # Joiners wait to receive data from sharer # Otherwise, load reflections from local store - if not self.shared_activity: + if not self.sharing: self.busy_cursor() GObject.idle_add(self._load_reflections) @@ -169,6 +163,7 @@ def _load_reflections(self): self._find_starred() self._reflect_window.load(self.reflection_data) self.reset_cursor() + self.send_new_reflection() def _found_obj_id(self, obj_id): for item in self.reflection_data: @@ -570,7 +565,7 @@ def _do_search(self): for i, tag in enumerate(tags): if not tag[0] == '#': tags[i] = '#%s' % tag - logging.error(tags) + logging.debug(tags) for item in self.reflection_data: hidden = True if 'tags' in item: @@ -709,259 +704,190 @@ def _close_alert_cb(self, alert, response_id): if response_id is Gtk.ResponseType.OK: self.close() - def _setup_presence_service(self): - ''' Setup the Presence Service. ''' - self.pservice = presenceservice.get_instance() + def set_data(self, data): + pass - owner = self.pservice.get_owner() - self.owner = owner - self._share = '' - self.connect('shared', self._shared_cb) - self.connect('joined', self._joined_cb) + def get_data(self): + return None def _shared_cb(self, activity): ''' Either set up initial share...''' - if self.shared_activity is None: - _logger.error('Failed to share or join activity ... \ - shared_activity is null in _shared_cb()') - return - - self.initiating = True - self._waiting_for_reflections = False - _logger.debug('I am sharing...') - - self.conn = self.shared_activity.telepathy_conn - self.tubes_chan = self.shared_activity.telepathy_tubes_chan - self.text_chan = self.shared_activity.telepathy_text_chan - - self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal( - 'NewTube', self._new_tube_cb) - - _logger.debug('This is my activity: making a tube...') - self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].OfferDBusTube( - SERVICE, {}) - - self.sharing = True + self.after_share_join(True) def _joined_cb(self, activity): ''' ...or join an exisiting share. ''' - if self.shared_activity is None: - _logger.error('Failed to share or join activity ... \ - shared_activity is null in _shared_cb()') - return - - if self._joined_alert is not None: - self.remove_alert(self._joined_alert) - self._joined_alert = None - - self.initiating = False - self._waiting_for_reflections = True - _logger.debug('I joined a shared activity.') - - self.conn = self.shared_activity.telepathy_conn - self.tubes_chan = self.shared_activity.telepathy_tubes_chan - self.text_chan = self.shared_activity.telepathy_text_chan - - self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal( - 'NewTube', self._new_tube_cb) - - _logger.debug('I am joining an activity: waiting for a tube...') - self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes( - reply_handler=self._list_tubes_reply_cb, - error_handler=self._list_tubes_error_cb) - - self.sharing = True - - def _list_tubes_reply_cb(self, tubes): - ''' Reply to a list request. ''' - for tube_info in tubes: - self._new_tube_cb(*tube_info) - - def _list_tubes_error_cb(self, e): - ''' Log errors. ''' - _logger.error('ListTubes() failed: %s', e) - - def _new_tube_cb(self, id, initiator, type, service, params, state): - ''' Create a new tube. ''' - _logger.debug('New tube: ID=%d initator=%d type=%d service=%s ' - 'params=%r state=%d', id, initiator, type, service, - params, state) - - if (type == telepathy.TUBE_TYPE_DBUS and service == SERVICE): - if state == telepathy.TUBE_STATE_LOCAL_PENDING: - self.tubes_chan[ - telepathy.CHANNEL_TYPE_TUBES].AcceptDBusTube(id) - - self.collab = CollabWrapper(self) - self.collab.message.connect(self.event_received_cb) - self.collab.setup() - - if self._waiting_for_reflections: - self.send_event(JOIN_CMD, {}) - self._joined_alert = Alert() - self._joined_alert.props.title = _('Please wait') - self._joined_alert.props.msg = _('Requesting reflections...') - self.add_alert(self._joined_alert) - - def event_received_cb(self, collab, buddy, msg): + self.after_share_join(False) + + def after_share_join(self, sharer): + self.initiating = sharer + self._waiting_for_reflections = not sharer + self._sharing = True + + def _setup_dispatch_table(self): + self._processing_methods = { + JOIN_CMD: [self.send_reflection_data, 'send reflection data'], + NEW_REFLECTION_CMD: [self.new_reflection, 'new reflection'], + TITLE_CMD: [self.receive_title, 'receive title'], + TAG_CMD: [self.receive_tags, 'receive tags'], + ACTIVITY_CMD: [self.receive_activity, 'receive activity'], + STAR_CMD: [self.receive_stars, 'receive stars'], + COMMENT_CMD: [self.receive_comment, 'receive comment'], + REFLECTION_CMD: [self.receive_reflection, 'receive reflection'], + IMAGE_REFLECTION_CMD: [self.receive_image_reflection, 'receive image reflection'], + PICTURE_CMD: [self.receive_picture, 'receive picture'], + SHARE_CMD: [self.receive_reflection_data, 'receive reflection data'], + + } + + def send_new_reflection(self): + if self._waiting_for_reflections: + self.send_event(JOIN_CMD, {}) + + def send_reflection_data(self, payload): + # Sharer needs to send reflections database to joiners. + if self.initiating: + # Send pictures first. + for item in self.reflection_data: + if 'content' in item: + for content in item['content']: + if 'image' in content: + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size( + content['image'], 120, 90) + if pixbuf is not None: + data = utils.pixbuf_to_base64(pixbuf) + self.send_event( + PICTURE_CMD, { + "image": os.path.basename( + content['image']), "data": data}) + data = json.dumps(self.reflection_data) + self.send_event(SHARE_CMD, {"data": data}) + + def receive_reflection_data(self, payload): + # Joiner needs to load reflection database. + if not self.initiating: + # Note that pictures should be received. + self.reflection_data = payload + self._reflect_window.load(self.reflection_data) + self._waiting_for_reflections = False + self.reset_cursor() + if self._joined_alert is not None: + self.remove_alert(self._joined_alert) + self._joined_alert = None + + def new_reflection(self, payload): + self._reflect_window.add_new_reflection(payload) + + def receive_title(self, payload): + obj_id = payload.get("obj_id") + title = payload.get("title") + for item in self.reflection_data: + if item['obj_id'] == obj_id: + found_the_object = True + self._reflect_window.update_title(obj_id, title) + break + if not found_the_object: + logging.error('Could not find obj_id %s' % obj_id) + + def receive_tags(self, payload): + obj_id = payload.get("obj_id") + data = payload.get("data") + for item in self.reflection_data: + if item['obj_id'] == obj_id: + found_the_object = True + self._reflect_window.update_tags(obj_id, data) + break + if not found_the_object: + logging.error('Could not find obj_id %s' % obj_id) + + def receive_activity(self, payload): + obj_id = payload.get("obj_id") + bundle_id = payload.get("bundle_id") + for item in self.reflection_data: + if item['obj_id'] == obj_id: + found_the_object = True + self._reflect_window.insert_activity(obj_id, bundle_id) + break + if not found_the_object: + logging.error('Could not find obj_id %s' % obj_id) + + def receive_stars(self, payload): + obj_id = payload.get("obj_id") + stars = payload.get("stars") + for item in self.reflection_data: + if item['obj_id'] == obj_id: + found_the_object = True + self._reflect_window.update_stars(obj_id, int(stars)) + break + if not found_the_object: + logging.error('Could not find obj_id %s' % obj_id) + + def receive_comment(self, payload): + found_the_object = False + # Receive a comment and associated reflection ID + obj_id = payload.get("obj_id") + nick = payload.get("nick") + color = payload.get("color") + comment = payload.get("comment") + for item in self.reflection_data: + if item['obj_id'] == obj_id: + found_the_object = True + if 'comments' not in item: + item['comments'] = [] + data = {'nick': nick, 'comment': comment, 'color': color} + item['comments'].append(data) + self._reflect_window.insert_comment(obj_id, data) + break + if not found_the_object: + logging.error('Could not find obj_id %s' % obj_id) + + def receive_reflection(self, payload): + found_the_object = False + # Receive a reflection and associated reflection ID + obj_id = payload.get("obj_id") + reflection = payload.get("reflection") + for item in self.reflection_data: + if item['obj_id'] == obj_id: + found_the_object = True + if '' not in item: + item['content'] = [] + item['content'].append({'text': reflection}) + self._reflect_window.insert_reflection(obj_id, reflection) + break + if not found_the_object: + logging.error('Could not find obj_id %s' % obj_id) + + def receive_image_reflection(self, payload): + found_the_object = False + # Receive a picture reflection and associated reflection ID + obj_id = payload.get("obj_id") + basename = payload.get("basename") + for item in self.reflection_data: + if item['obj_id'] == obj_id: + found_the_object = True + if '' not in item: + item['content'] = [] + item['content'].append( + {'image': os.path.join(self.tmp_path, basename)}) + self._reflect_window.insert_picture( + obj_id, os.path.join(self.tmp_path, basename)) + break + if not found_the_object: + logging.error('Could not find obj_id %s' % obj_id) + + def receive_picture(self, payload): + # Receive a picture (MAYBE DISPLAY IT AS IT ARRIVES?) + basename = payload.get("basename") + data = payload.get("data") + utils.base64_to_file(data, os.path.join(self.tmp_path, basename)) + + def _message_cb(self, collab, buddy, msg): ''' Data is passed as tuples: cmd:text ''' command = msg.get("command") payload = msg.get("payload") logging.debug(command) - - if command == JOIN_CMD: - # Sharer needs to send reflections database to joiners. - if self.initiating: - # Send pictures first. - for item in self.reflection_data: - if 'content' in item: - for content in item['content']: - if 'image' in content: - pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size( - content['image'], 120, 90) - if pixbuf is not None: - data = utils.pixbuf_to_base64(pixbuf) - self.send_event( - PICTURE_CMD, { - "image": os.path.basename( - content['image']), "data": data}) - data = json.dumps(self.reflection_data) - self.send_event(SHARE_CMD, {"data": data}) - elif command == NEW_REFLECTION_CMD: - self._reflect_window.add_new_reflection(payload) - elif command == TITLE_CMD: - obj_id = payload.get("obj_id") - title = payload.get("title") - for item in self.reflection_data: - if item['obj_id'] == obj_id: - found_the_object = True - self._reflect_window.update_title(obj_id, title) - break - if not found_the_object: - logging.error('Could not find obj_id %s' % obj_id) - elif command == TAG_CMD: - obj_id = payload.get("obj_id") - data = payload.get("data") - for item in self.reflection_data: - if item['obj_id'] == obj_id: - found_the_object = True - self._reflect_window.update_tags(obj_id, data) - break - if not found_the_object: - logging.error('Could not find obj_id %s' % obj_id) - elif command == ACTIVITY_CMD: - obj_id = payload.get("obj_id") - bundle_id = payload.get("bundle_id") - for item in self.reflection_data: - if item['obj_id'] == obj_id: - found_the_object = True - self._reflect_window.insert_activity(obj_id, bundle_id) - break - if not found_the_object: - logging.error('Could not find obj_id %s' % obj_id) - elif command == STAR_CMD: - obj_id = payload.get("obj_id") - stars = payload.get("stars") - for item in self.reflection_data: - if item['obj_id'] == obj_id: - found_the_object = True - self._reflect_window.update_stars(obj_id, int(stars)) - break - if not found_the_object: - logging.error('Could not find obj_id %s' % obj_id) - elif command == COMMENT_CMD: - found_the_object = False - # Receive a comment and associated reflection ID - obj_id = payload.get("obj_id") - nick = payload.get("nick") - color = payload.get("color") - comment = payload.get("comment") - for item in self.reflection_data: - if item['obj_id'] == obj_id: - found_the_object = True - if 'comments' not in item: - item['comments'] = [] - data = {'nick': nick, 'comment': comment, 'color': color} - item['comments'].append(data) - self._reflect_window.insert_comment(obj_id, data) - break - if not found_the_object: - logging.error('Could not find obj_id %s' % obj_id) - elif command == REFLECTION_CMD: - found_the_object = False - # Receive a reflection and associated reflection ID - obj_id = payload.get("obj_id") - reflection = payload.get("reflection") - for item in self.reflection_data: - if item['obj_id'] == obj_id: - found_the_object = True - if '' not in item: - item['content'] = [] - item['content'].append({'text': reflection}) - self._reflect_window.insert_reflection(obj_id, reflection) - break - if not found_the_object: - logging.error('Could not find obj_id %s' % obj_id) - elif command == IMAGE_REFLECTION_CMD: - found_the_object = False - # Receive a picture reflection and associated reflection ID - obj_id = payload.get("obj_id") - basename = payload.get("basename") - for item in self.reflection_data: - if item['obj_id'] == obj_id: - found_the_object = True - if '' not in item: - item['content'] = [] - item['content'].append( - {'image': os.path.join(self.tmp_path, basename)}) - self._reflect_window.insert_picture( - obj_id, os.path.join(self.tmp_path, basename)) - break - if not found_the_object: - logging.error('Could not find obj_id %s' % obj_id) - elif command == PICTURE_CMD: - # Receive a picture (MAYBE DISPLAY IT AS IT ARRIVES?) - basename = payload.get("basename") - data = payload.get("data") - utils.base64_to_file(data, os.path.join(self.tmp_path, basename)) - elif command == SHARE_CMD: - # Joiner needs to load reflection database. - if not self.initiating: - # Note that pictures should be received. - self.reflection_data = payload - self._reflect_window.load(self.reflection_data) - self._waiting_for_reflections = False - self.reset_cursor() - if self._joined_alert is not None: - self.remove_alert(self._joined_alert) - self._joined_alert = None + self._processing_methods[command][0](payload) def send_event(self, command, data): ''' Send event through the tube. ''' - if hasattr(self, 'collab') and self.collab is not None: - data["command"] = command - self.collab.post(data) - - -class ChatTube(ExportedGObject): - ''' Class for setting up tube for sharing ''' - - def __init__(self, tube, is_initiator, stack_received_cb): - super(ChatTube, self).__init__(tube, PATH) - self.tube = tube - self.is_initiator = is_initiator # Are we sharing or joining activity? - self.stack_received_cb = stack_received_cb - self.stack = '' - - self.tube.add_signal_receiver(self.send_stack_cb, 'SendText', IFACE, - path=PATH, sender_keyword='sender') - - def send_stack_cb(self, text, sender=None): - if sender == self.tube.get_unique_name(): - return - self.stack = text - self.stack_received_cb(text) - - @signal(dbus_interface=IFACE, signature='s') - def SendText(self, text): - self.stack = text + data["command"] = command + self.collab.post(data) diff --git a/activity/activity.info b/activity/activity.info index eae5519..76219ae 100644 --- a/activity/activity.info +++ b/activity/activity.info @@ -1,6 +1,6 @@ [Activity] name = Reflect -exec = sugar-activity activity.ReflectActivity +exec = sugar-activity3 activity.ReflectActivity bundle_id = org.sugarlabs.Reflect icon = reflect activity_version = 1.0 @@ -8,4 +8,4 @@ show_launcher = yes license = GPLv3 single_instance = yes summary = a tool for reflecting on your work -repository = https://github.com/walterbender/reflect +repository = https://github.com/sugarlabs/reflect.git diff --git a/textchannelwrapper.py b/collabwrapper.py similarity index 73% rename from textchannelwrapper.py rename to collabwrapper.py index a4c3fec..44ccfcf 100644 --- a/textchannelwrapper.py +++ b/collabwrapper.py @@ -16,40 +16,51 @@ # Foundation, 51 Franklin Street, Suite 500 Boston, MA 02110-1335 USA ''' -The wrapper module provides an abstraction over the sugar +The wrapper module provides an abstraction over the Sugar collaboration system. + Using CollabWrapper ------------------- -1. Implement the `get_data` and `set_data` methods in your activity - class:: +1. Add `get_data` and `set_data` methods to the activity class:: + def get_data(self): # return plain python objects - things that can be encoded # using the json module return dict( text=self._entry.get_text() ) + def set_data(self, data): # data will be the same object returned by get_data self._entry.set_text(data.get('text')) -2. Make your CollabWrapper instance:: + +2. Make a CollabWrapper instance:: + def __init__(self, handle): sugar3.activity.activity.Activity.__init__(self, handle) self._collab = CollabWrapper(self) self._collab.connect('message', self.__message_cb) - # setup your activity + + # setup your activity here + self._collab.setup() -3. Post any changes to the CollabWrapper. The changes will be sent to - other users if any are connected:: + +3. Post any changes of shared state to the CollabWrapper. The changes + will be sent to other buddies if any are connected, for example:: + def __entry_changed_cb(self, *args): self._collab.post(dict( action='entry_changed', new_text=self._entry.get_text() )) -4. Handle incoming messages:: - def __message_cb(self, collab, buddy, message): + +4. Handle incoming messages, for example:: + + def __message_cb(self, collab, buddy, msg): action = msg.get('action') if action == 'entry_changed': self._entry.set_text(msg.get('new_text')) + ''' import os @@ -57,34 +68,36 @@ def __message_cb(self, collab, buddy, message): import socket from gettext import gettext as _ +import gi +gi.require_version('TelepathyGLib', '0.12') from gi.repository import GObject from gi.repository import Gio from gi.repository import GLib +from gi.repository import TelepathyGLib import dbus - -from telepathy.interfaces import \ - CHANNEL_INTERFACE, \ - CHANNEL_INTERFACE_GROUP, \ - CHANNEL_TYPE_TEXT, \ - CHANNEL_TYPE_FILE_TRANSFER, \ - CONN_INTERFACE_ALIASING, \ - CHANNEL, \ - CLIENT -from telepathy.constants import \ - CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES, \ - CONNECTION_HANDLE_TYPE_CONTACT, \ - CHANNEL_TEXT_MESSAGE_TYPE_NORMAL, \ - CONNECTION_HANDLE_TYPE_CONTACT, \ - SOCKET_ADDRESS_TYPE_UNIX, \ - SOCKET_ACCESS_CONTROL_LOCALHOST -from telepathy.client import Connection, Channel +from dbus import PROPERTIES_IFACE + +CHANNEL_INTERFACE = TelepathyGLib.IFACE_CHANNEL +CHANNEL_INTERFACE_GROUP = TelepathyGLib.IFACE_CHANNEL_INTERFACE_GROUP +CHANNEL_TYPE_TEXT = TelepathyGLib.IFACE_CHANNEL_TYPE_TEXT +CHANNEL_TYPE_FILE_TRANSFER = TelepathyGLib.IFACE_CHANNEL_TYPE_FILE_TRANSFER +CONN_INTERFACE_ALIASING = TelepathyGLib.IFACE_CONNECTION_INTERFACE_ALIASING +CONN_INTERFACE = TelepathyGLib.IFACE_CONNECTION +CHANNEL = TelepathyGLib.IFACE_CHANNEL +CLIENT = TelepathyGLib.IFACE_CLIENT +CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES = \ + TelepathyGLib.ChannelGroupFlags.CHANNEL_SPECIFIC_HANDLES +CONNECTION_HANDLE_TYPE_CONTACT = TelepathyGLib.HandleType.CONTACT +CHANNEL_TEXT_MESSAGE_TYPE_NORMAL = TelepathyGLib.ChannelTextMessageType.NORMAL +SOCKET_ADDRESS_TYPE_UNIX = TelepathyGLib.SocketAddressType.UNIX +SOCKET_ACCESS_CONTROL_LOCALHOST = TelepathyGLib.SocketAccessControl.LOCALHOST from sugar3.presence import presenceservice from sugar3.activity.activity import SCOPE_PRIVATE from sugar3.graphics.alert import NotifyAlert import logging -_logger = logging.getLogger('text-channel-wrapper') +_logger = logging.getLogger('CollabWrapper') ACTION_INIT_REQUEST = '!!ACTION_INIT_REQUEST' ACTION_INIT_RESPONSE = '!!ACTION_INIT_RESPONSE' @@ -93,31 +106,52 @@ def __message_cb(self, collab, buddy, message): class CollabWrapper(GObject.GObject): ''' - The collaboration wrapper provides a high level abstraction over the - collaboration system. The wrapper deals with setting up the channels, - encoding and decoding messages, initialization and alerting the user - to the status. - When a user joins the activity, it will query the leader for the - contents. The leader will return the result of the activity's - `get_data` function which will be passed to the `set_data` function - on the new user's computer. - The `message` signal is called when a message is received from a - buddy. It has 2 arguments. The first is the buddy, as a - :class:`sugar3.presence.buddy.Buddy`. The second is the decoded - content of the message, same as that posted by the other instance. - The `joined` signal is emitted when the buddy joins a running - activity. If the user shares and activity, the joined signal - is not emitted. By the time this signal is emitted, the channels - will be setup so all messages will flow through. - The `buddy_joined` and `buddy_left` signals are emitted when - another user joins or leaves the activity. They both a - :class:`sugar3.presence.buddy.Buddy` as their only argument. + The wrapper provides a high level abstraction over the + collaboration system. The wrapper deals with setting up the + channels, encoding and decoding messages, initialization and + alerting the caller to the status. + + An activity instance is initially private, but may be shared. Once + shared, an instance will remain shared for as long as the activity + runs. On stop, the journal will preserve the instance as shared, + and on resume the instance will be shared again. + + When the caller shares an activity instance, they are the leader, + and other buddies may join. The instance is now a shared activity. + + When the caller joins a shared activity, the leader will call + `get_data`, and the caller's `set_data` will be called with the + result. + + The `joined` signal is emitted when the caller joins a shared + activity. One or more `buddy_joined` signals will be emitted before + this signal. The signal is not emitted to the caller who first + shared the activity. There are no arguments. + + The `buddy_joined` signal is emitted when another buddy joins the + shared activity. At least one will be emitted before the `joined` + signal. The caller will never be mentioned, but is assumed to be + part of the set. The signal passes a + :class:`sugar3.presence.buddy.Buddy` as the only argument. + + The `buddy_left` signal is emitted when another user leaves the + shared activity. The signal is not emitted during quit. The signal + passes a :class:`sugar3.presence.buddy.Buddy` as the only argument. + + Any buddy may call `post` to send a message to all buddies. Each + buddy will receive a `message` signal. + + The `message` signal is emitted when a `post` is received from any + buddy. The signal has two arguments. The first is a + :class:`sugar3.presence.buddy.Buddy`. The second is the message. + + Any buddy may call `send_file_memory` or `send_file_file` to + transfer a file to all buddies. A description is to be given. + Each buddy will receive an `incoming_file` signal. + The `incoming_file` signal is emitted when a file transfer is - received from a buddy. The first argument is the object representing - the transfer, as a - :class:`sugar3.presence.filetransfer.IncomingFileTransfer`. The seccond - argument is the description, as passed to the `send_file_*` function - on the sender's client + received. The signal has two arguments. The first is a + :class:`IncomingFileTransfer`. The second is the description. ''' message = GObject.Signal('message', arg_types=[object, object]) @@ -127,24 +161,28 @@ class CollabWrapper(GObject.GObject): incoming_file = GObject.Signal('incoming_file', arg_types=[object, object]) def __init__(self, activity): + _logger.debug('__init__') GObject.GObject.__init__(self) self.activity = activity self.shared_activity = activity.shared_activity self._leader = False self._init_waiting = False self._text_channel = None + self._owner = presenceservice.get_instance().get_owner() def setup(self): ''' - Setup must be called to so that the activity can join or share + Setup must be called so that the activity can join or share if appropriate. + .. note:: As soon as setup is called, any signal, `get_data` or - `set_data` call must be made. This means that your - activity must have set up enough so these functions can - work. For example, place this at the end of the activity's + `set_data` call may occur. This means that the activity + must have set up enough so these functions can work. For + example, call setup at the end of the activity `__init__` function. ''' + _logger.debug('setup') # Some glue to know if we are launching, joining, or resuming # a shared activity. if self.shared_activity: @@ -159,6 +197,7 @@ def setup(self): self._alert(_('Joining activity...'), _('Please wait for the connection...')) else: + self._leader = True if not self.activity.metadata or self.activity.metadata.get( 'share-scope', SCOPE_PRIVATE) == \ SCOPE_PRIVATE: @@ -181,15 +220,15 @@ def _alert(self, title, msg=None): def __shared_cb(self, sender): ''' Callback for when activity is shared. ''' + _logger.debug('__shared_cb') + # FIXME: may be called twice, but we should only act once self.shared_activity = self.activity.shared_activity self._setup_text_channel() self._listen_for_channels() - self._leader = True - _logger.debug('I am sharing...') - def __joined_cb(self, sender): '''Callback for when an activity is joined.''' + _logger.debug('__joined_cb') self.shared_activity = self.activity.shared_activity if not self.shared_activity: return @@ -199,11 +238,14 @@ def __joined_cb(self, sender): self._init_waiting = True self.post({'action': ACTION_INIT_REQUEST}) - _logger.debug('I joined a shared activity.') + for buddy in self.shared_activity.get_joined_buddies(): + self.buddy_joined.emit(buddy) + self.joined.emit() def _setup_text_channel(self): ''' Set up a text channel to use for collaboration. ''' + _logger.debug('_setup_text_channel') self._text_channel = _TextChannelWrapper( self.shared_activity.telepathy_text_chan, self.shared_activity.telepathy_conn) @@ -218,10 +260,12 @@ def _setup_text_channel(self): self.shared_activity.connect('buddy-left', self.__buddy_left_cb) def _listen_for_channels(self): + _logger.debug('_listen_for_channels') conn = self.shared_activity.telepathy_conn conn.connect_to_signal('NewChannels', self.__new_channels_cb) def __new_channels_cb(self, channels): + _logger.debug('__new_channels_cb') conn = self.shared_activity.telepathy_conn for path, props in channels: if props[CHANNEL + '.Requested']: @@ -232,39 +276,43 @@ def __new_channels_cb(self, channels): self._handle_ft_channel(conn, path, props) def _handle_ft_channel(self, conn, path, props): + _logger.debug('_handle_ft_channel') ft = IncomingFileTransfer(conn, path, props) if ft.description == ACTION_INIT_RESPONSE: - ft.connect('notify::state', self.__notify_ft_state_cb) + ft.connect('ready', self.__ready_cb) ft.accept_to_memory() else: desc = json.loads(ft.description) self.incoming_file.emit(ft, desc) - def __notify_ft_state_cb(self, ft, pspec): - if ft.props.state == FT_STATE_COMPLETED and self._init_waiting: - stream = ft.props.output + def __ready_cb(self, ft, stream): + _logger.debug('__ready_cb') + if self._init_waiting: stream.close(None) # FIXME: The data prop seems to just be the raw pointer gbytes = stream.steal_as_bytes() data = gbytes.get_data() - logging.debug('Got init data from buddy: %s', data) + _logger.debug('Got init data from buddy: %r', data) data = json.loads(data) self.activity.set_data(data) self._init_waiting = False def __received_cb(self, buddy, msg): '''Process a message when it is received.''' + _logger.debug('__received_cb') action = msg.get('action') - if action == ACTION_INIT_REQUEST and self._leader: - data = self.activity.get_data() - data = json.dumps(data) - OutgoingBlobTransfer( - buddy, - self.shared_activity.telepathy_conn, - data, - self.get_client_name(), - ACTION_INIT_RESPONSE, - ACTIVITY_FT_MIME) + if action == ACTION_INIT_REQUEST: + if self._leader: + data = self.activity.get_data() + if data is not None: + data = json.dumps(data) + OutgoingBlobTransfer( + buddy, + self.shared_activity.telepathy_conn, + data, + self.get_client_name(), + ACTION_INIT_RESPONSE, + ACTIVITY_FT_MIME) return if buddy: @@ -276,15 +324,16 @@ def __received_cb(self, buddy, msg): def send_file_memory(self, buddy, data, description): ''' - Send a 1-to-1 transfer from memory to a given buddy. They will - get the file transfer and description through the `incoming_transfer` - signal. + Send a one to one file transfer from memory to a buddy. The + buddy will get the file transfer and description through the + `incoming_transfer` signal. + Args: - buddy (sugar3.presence.buddy.Buddy), buddy to offer the transfer to - data (str), the data to offer to the buddy via the transfer + buddy (sugar3.presence.buddy.Buddy), buddy to send to. + data (str), the data to send. description (object), a json encodable description for the - transfer. This will be given to the `incoming_transfer` signal - of the transfer + transfer. This will be given to the + `incoming_transfer` signal at the buddy. ''' OutgoingBlobTransfer( buddy, @@ -296,15 +345,16 @@ def send_file_memory(self, buddy, data, description): def send_file_file(self, buddy, path, description): ''' - Send a 1-to-1 transfer from a file to a given buddy. They will - get the file transfer and description through the `incoming_transfer` - signal. + Send a one to one file transfer from a filesystem path to a + given buddy. The buddy will get the file transfer and + description through the `incoming_transfer` signal. + Args: - buddy (sugar3.presence.buddy.Buddy), buddy to offer the transfer to - path (str), path of the file to send to the buddy + buddy (sugar3.presence.buddy.Buddy), buddy to send to. + path (str), path of the file containing the data to send. description (object), a json encodable description for the - transfer. This will be given to the `incoming_transfer` signal - of the transfer + transfer. This will be given to the + `incoming_transfer` signal at the buddy. ''' OutgoingFileTransfer( buddy, @@ -316,12 +366,12 @@ def send_file_file(self, buddy, path, description): def post(self, msg): ''' - Broadcast a message to the other buddies if the activity is - shared. If it is not shared, the message will not be send - at all. + Send a message to all buddies. If the activity is not shared, + no message is sent. + Args: - msg (object): json encodable object to send to the other - buddies, eg. :class:`dict` or :class:`str`. + msg (object): json encodable object to send, + eg. :class:`dict` or :class:`str`. ''' if self._text_channel is not None: self._text_channel.post(msg) @@ -337,10 +387,27 @@ def __buddy_left_cb(self, sender, buddy): def get_client_name(self): ''' Get the name of the activity's telepathy client. + Returns: str, telepathy client name ''' return CLIENT + '.' + self.activity.get_bundle_id() + @GObject.Property + def leader(self): + ''' + Boolean of if this client is the leader in this activity. The + way the leader is decided may change, however there should only + ever be one leader for an activity. + ''' + return self._leader + + @GObject.Property + def owner(self): + ''' + Ourselves, :class:`sugar3.presence.buddy.Owner` + ''' + return self._owner + FT_STATE_NONE = 0 FT_STATE_PENDING = 1 @@ -362,6 +429,7 @@ class _BaseFileTransfer(GObject.GObject): ''' The base file transfer should not be used directly. It is used as a base class for the incoming and outgoing file transfers. + Props: filename (str), metadata provided by the buddy file_size (str), size of the file being sent/received, in bytes @@ -370,9 +438,10 @@ class _BaseFileTransfer(GObject.GObject): buddy (:class:`sugar3.presence.buddy.Buddy`), other party in the transfer reason_last_change (FT_REASON_*), reason for the last state change + GObject Props: state (FT_STATE_*), current state of the transfer - transferred_bytes (int), number of bytes transfered so far + transferred_bytes (int), number of bytes transferred so far ''' def __init__(self): @@ -401,7 +470,7 @@ def set_channel(self, channel): self.channel[CHANNEL_TYPE_FILE_TRANSFER].connect_to_signal( 'InitialOffsetDefined', self.__initial_offset_defined_cb) - channel_properties = self.channel[dbus.PROPERTIES_IFACE] + channel_properties = self.channel[PROPERTIES_IFACE] props = channel_properties.GetAll(CHANNEL_TYPE_FILE_TRANSFER) self._state = props['State'] @@ -411,7 +480,7 @@ def set_channel(self, channel): self.mime_type = props['ContentType'] def __transferred_bytes_changed_cb(self, transferred_bytes): - logging.debug('__transferred_bytes_changed_cb %r', transferred_bytes) + _logger.debug('__transferred_bytes_changed_cb %r', transferred_bytes) self.props.transferred_bytes = transferred_bytes def _set_transferred_bytes(self, transferred_bytes): @@ -426,11 +495,11 @@ def _get_transferred_bytes(self): setter=_set_transferred_bytes) def __initial_offset_defined_cb(self, offset): - logging.debug('__initial_offset_defined_cb %r', offset) + _logger.debug('__initial_offset_defined_cb %r', offset) self.initial_offset = offset def __state_changed_cb(self, state, reason): - logging.debug('__state_changed_cb %r %r', state, reason) + _logger.debug('__state_changed_cb %r %r', state, reason) self.reason_last_change = reason self.props.state = state @@ -445,6 +514,7 @@ def _get_state(self): def cancel(self): ''' Request that telepathy close the file transfer channel + Spec: http://telepathy.freedesktop.org/spec/Channel.html#Method:Close ''' self.channel[CHANNEL].Close() @@ -457,16 +527,24 @@ class IncomingFileTransfer(_BaseFileTransfer): to the state and wait until the transfer is completed. Then you can read the file that it was saved to, or access the :class:`Gio.MemoryOutputStream` from the `output` property. + The `output` property is different depending on how the file was accepted. If the file was accepted to a file on the file system, it is a string representing the path to the file. If the file was accepted to memory, it is a :class:`Gio.MemoryOutputStream`. ''' + ready = GObject.Signal('ready', arg_types=[object]) + def __init__(self, connection, object_path, props): _BaseFileTransfer.__init__(self) - channel = Channel(connection.bus_name, object_path) + channel = {} + proxy = dbus.Bus().get_object(connection.bus_name, object_path) + channel[PROPERTIES_IFACE] = dbus.Interface(proxy, PROPERTIES_IFACE) + channel[CHANNEL] = dbus.Interface(proxy, CHANNEL) + channel[CHANNEL_TYPE_FILE_TRANSFER] = dbus.Interface( + proxy, CHANNEL_TYPE_FILE_TRANSFER) self.set_channel(channel) self.connect('notify::state', self.__notify_state_cb) @@ -481,6 +559,7 @@ def accept_to_file(self, destination_path): ''' Accept the file transfer and write it to a new file. The file must already exist. + Args: destination_path (str): the path where a new file will be created and saved to @@ -497,6 +576,7 @@ def accept_to_memory(self): Accept the file transfer. Once the state is FT_STATE_OPEN, a :class:`Gio.MemoryOutputStream` accessible via the output prop. ''' + self._destination_path = None self._accept() def _accept(self): @@ -509,7 +589,7 @@ def _accept(self): byte_arrays=True) def __notify_state_cb(self, file_transfer, pspec): - logging.debug('__notify_state_cb %r', self.props.state) + _logger.debug('__notify_state_cb %r', self.props.state) if self.props.state == FT_STATE_OPEN: # Need to hold a reference to the socket so that python doesn't # close the fd when it goes out of scope @@ -526,13 +606,21 @@ def __notify_state_cb(self, file_transfer, pspec): else: self._output_stream = destination_file.append_to() else: - self._output_stream = Gio.MemoryOutputStream.new_resizable() + if hasattr(Gio.MemoryOutputStream, 'new_resizable'): + self._output_stream = \ + Gio.MemoryOutputStream.new_resizable() + else: + self._output_stream = Gio.MemoryOutputStream() self._output_stream.splice_async( input_stream, Gio.OutputStreamSpliceFlags.CLOSE_SOURCE | Gio.OutputStreamSpliceFlags.CLOSE_TARGET, - GLib.PRIORITY_LOW, None, None, None) + GLib.PRIORITY_LOW, None, self.__splice_done_cb, None) + + def __splice_done_cb(self, output_stream, res, user): + _logger.debug('__splice_done_cb') + self.ready.emit(self._destination_path or self._output_stream) @GObject.Property def output(self): @@ -542,10 +630,12 @@ def output(self): class _BaseOutgoingTransfer(_BaseFileTransfer): ''' This class provides the base of an outgoing file transfer. + You can override the `_get_input_stream` method to return any type of Gio input stream. This will then be used to provide the file if requested by the application. You also need to call `_create_channel` with the length of the file in bytes during your `__init__`. + Args: buddy (sugar3.presence.buddy.Buddy), who to send the transfer to conn (telepathy.client.conn.Connection), telepathy connection to @@ -578,7 +668,13 @@ def _create_channel(self, file_size): CHANNEL_TYPE_FILE_TRANSFER + '.Size': file_size, CHANNEL_TYPE_FILE_TRANSFER + '.ContentType': self._mime, CHANNEL_TYPE_FILE_TRANSFER + '.InitialOffset': 0}, signature='sv')) - self.set_channel(Channel(self._conn.bus_name, object_path)) + channel = {} + proxy = dbus.Bus().get_object(self._conn.bus_name, object_path) + channel[PROPERTIES_IFACE] = dbus.Interface(proxy, PROPERTIES_IFACE) + channel[CHANNEL] = dbus.Interface(proxy, CHANNEL) + channel[CHANNEL_TYPE_FILE_TRANSFER] = dbus.Interface( + proxy, CHANNEL_TYPE_FILE_TRANSFER) + self.set_channel(channel) channel_file_transfer = self.channel[CHANNEL_TYPE_FILE_TRANSFER] self._socket_address = channel_file_transfer.ProvideFile( @@ -609,8 +705,10 @@ class OutgoingFileTransfer(_BaseOutgoingTransfer): ''' An outgoing file transfer to send from a file (on the computer's file system). + Note that the `path` argument is the path for the file that will be sent, whereas the `filename` argument is only for metadata. + Args: path (str), path of the file to send ''' @@ -624,13 +722,13 @@ def __init__(self, buddy, conn, path, filename, description, mime): self._create_channel(file_size) def _get_input_stream(self): - logging.debug('opening %s for reading', self._file_name) - input_stream = Gio.File.new_for_path(self._file_name).read(None) + return Gio.File.new_for_path(self._path).read(None) class OutgoingBlobTransfer(_BaseOutgoingTransfer): ''' An outgoing file transfer to send from a string in memory. + Args: blob (str), data to send ''' @@ -639,7 +737,7 @@ def __init__(self, buddy, conn, blob, filename, description, mime): _BaseOutgoingTransfer.__init__( self, buddy, conn, filename, description, mime) - self._blob = blob + self._blob = blob.encode('utf-8') self._create_channel(len(self._blob)) def _get_input_stream(self): @@ -693,6 +791,7 @@ def _closed_cb(self): def set_received_callback(self, callback): '''Connect the function callback to the signal. + callback -- callback function taking buddy and text args ''' if self._text_chan is None: @@ -711,6 +810,7 @@ def handle_pending_messages(self): def _received_cb(self, identity, timestamp, sender, type_, flags, text): '''Handle received text from the text channel. + Converts sender to a Buddy. Calls self._activity_cb which is a callback to the activity. ''' @@ -729,12 +829,12 @@ def _received_cb(self, identity, timestamp, sender, type_, flags, text): nick = self._conn[ CONN_INTERFACE_ALIASING].RequestAliases([sender])[0] buddy = {'nick': nick, 'color': '#000000,#808080'} - _logger.debug('exception: recieved from sender %r buddy %r' % + _logger.debug('exception: received from sender %r buddy %r' % (sender, buddy)) else: # XXX: cache these buddy = self._get_buddy(sender) - _logger.debug('Else: recieved from sender %r buddy %r' % + _logger.debug('Else: received from sender %r buddy %r' % (sender, buddy)) self._activity_cb(buddy, msg) @@ -747,7 +847,9 @@ def _received_cb(self, identity, timestamp, sender, type_, flags, text): def set_closed_callback(self, callback): '''Connect a callback for when the text channel is closed. + callback -- callback function taking no args + ''' _logger.debug('set closed callback') self._activity_close_cb = callback @@ -762,13 +864,14 @@ def _get_buddy(self, cs_handle): # Get the Telepathy Connection tp_name, tp_path = pservice.get_preferred_connection() - conn = Connection(tp_name, tp_path) + obj = dbus.Bus().get_object(tp_name, tp_path) + conn = dbus.Interface(obj, CONN_INTERFACE) group = self._text_chan[CHANNEL_INTERFACE_GROUP] my_csh = group.GetSelfHandle() if my_csh == cs_handle: handle = conn.GetSelfHandle() - elif (group.GetGroupFlags() & - CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES): + elif group.GetGroupFlags() & \ + CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES: handle = group.GetHandleOwners([cs_handle])[0] else: handle = cs_handle diff --git a/reflectwindow.py b/reflectwindow.py index 4650de6..cc2e6b9 100644 --- a/reflectwindow.py +++ b/reflectwindow.py @@ -85,7 +85,7 @@ def reload(self, reflection_data): self.load(reflection_data) def load(self, reflection_data): - if self._activity.initiating: + if self._activity.collab.props.leader: row = 1 # 0 is the entry for new reflections else: row = 0 diff --git a/utils.py b/utils.py index a52649a..ee90f8b 100644 --- a/utils.py +++ b/utils.py @@ -15,16 +15,15 @@ import subprocess import dbus import stat -import statvfs import glob -import urllib +import urllib.request, urllib.parse, urllib.error from random import uniform import tempfile import cairo import email.utils import re import time -from ConfigParser import ConfigParser +from configparser import ConfigParser from gi.repository import Vte from gi.repository import Gio @@ -32,7 +31,6 @@ from gi.repository import GdkPixbuf from gi.repository import Gtk from gi.repository import GLib -from gi.repository import GConf from gi.repository import GObject from sugar3 import env @@ -138,7 +136,7 @@ def _find_bundles(): 'activity.info')) for path in info_files: - fd = open(path, 'rb') + fd = open(path, 'r') cp = ConfigParser() cp.readfp(fd) section = 'Activity' @@ -360,7 +358,7 @@ def check_volume_suffix(volume_file): return TRAINING_DATA % volume_file[-13:] elif volume_file.endswith('.bin'): # See SEP-33 new_volume_file = volume_file[:-4] + TRAINING_SUFFIX - print new_volume_file + print(new_volume_file) os.rename(volume_file, new_volume_file) _logger.debug('return %s' % (TRAINING_DATA % new_volume_file[-13:])) return TRAINING_DATA % new_volume_file[-13:] @@ -508,8 +506,8 @@ def unexpected_training_data_files(path, name): def is_full(path, required=_MINIMUM_SPACE): ''' Make sure we have some room to write our data ''' volume_status = os.statvfs(path) - free_space = volume_status[statvfs.F_BSIZE] * \ - volume_status[statvfs.F_BAVAIL] + free_space = volume_status[os.statvfs().f_bsize] * \ + volume_status[os.statvfs().f_bavail] _logger.debug('free space: %d MB' % int(free_space / (1024 * 1024))) if free_space < required: _logger.error('free space: %d MB' % int(free_space / (1024 * 1024))) @@ -534,7 +532,7 @@ def is_landscape(): def get_safe_text(text): - return urllib.pathname2url(text.encode('ascii', 'xmlcharrefreplace')) + return urllib.request.pathname2url(text.encode('ascii', 'xmlcharrefreplace')) def get_battery_level(): @@ -556,8 +554,9 @@ def get_battery_level(): def get_sound_level(): - client = GConf.Client.get_default() - return client.get_int('/desktop/sugar/sound/volume') + settings = Gio.Settings('org.sugarlabs.sound') + volume = settings.get_int('volume') + return volume def is_clipboard_text_available(): @@ -983,8 +982,9 @@ def get_launch_count(activity): def get_colors(): - client = GConf.Client.get_default() - return XoColor(client.get_string('/desktop/sugar/user/color')) + settings = Gio.Settings('org.sugarlabs.user') + color = settings.get_string('color') + return XoColor(color) def get_nick(): @@ -1079,7 +1079,7 @@ def get_odt(): def get_speak_settings(activity): file_path = activity.file_path try: - configuration = json.loads(file(file_path, 'r').read()) + configuration = json.loads(open(file_path, 'r').read()) status = json.loads(configuration['status']) except Exception: # Ignore: Speak activity has not yet written out its data. @@ -1095,7 +1095,7 @@ def uitree_dump(): try: return json.loads(dbus.Interface(proxy, _DBUS_SERVICE).Dump()) except Exception as e: - print ('ERROR calling Dump: %s' % e) + print('ERROR calling Dump: %s' % e) # _logger.error('ERROR calling Dump: %s' % e) return ''