# Copyright (C) 2009 Jeff Ling # Copyright (C) 2010 Yann Leboulanger # # This file is part of the Whiteboard Plugin. # # Gajim is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published # by the Free Software Foundation; version 3 only. # # Gajim is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Gajim. If not, see . # ''' Whiteboard plugin. :author: Yann Leboulanger :since: 1st November 2010 :copyright: Copyright (2010) Yann Leboulanger :license: GPL ''' from urllib.parse import quote from gi.repository import Gio from gi.repository import GLib from nbxmpp import Message from gajim import common from gajim.common import app from gajim.common import ged from gajim.common import helpers from gajim.common.jingle_session import JingleSession from gajim.common.jingle_content import JingleContent from gajim.common.jingle_transport import JingleTransport from gajim.common.jingle_transport import TransportType from gajim.gui.dialogs import DialogButton from gajim.gui.dialogs import ConfirmationDialog from gajim.plugins import GajimPlugin from gajim.plugins.gajimplugin import GajimPluginException from gajim.plugins.helpers import log_calls from gajim.plugins.helpers import log from gajim.plugins.plugins_i18n import _ from whiteboard.whiteboard_widget import Whiteboard from whiteboard.whiteboard_widget import HAS_GOOCANVAS NS_JINGLE_XHTML = 'urn:xmpp:tmp:jingle:apps:xhtml' NS_JINGLE_SXE = 'urn:xmpp:tmp:jingle:transports:sxe' NS_SXE = 'urn:xmpp:sxe:0' class WhiteboardPlugin(GajimPlugin): @log_calls('WhiteboardPlugin') def init(self): self.config_dialog = None self.events_handlers = { 'jingle-request-received': (ged.GUI1, self._nec_jingle_received), 'jingle-connected-received': (ged.GUI1, self._nec_jingle_connected), 'jingle-disconnected-received': (ged.GUI1, self._nec_jingle_disconnected), 'raw-message-received': (ged.GUI1, self._nec_raw_message), } self.gui_extension_points = { 'chat_control': (self.connect_with_chat_control, self.disconnect_from_chat_control), 'chat_control_base_update_toolbar': (self.update_button_state, None), 'update_caps': (self._update_caps, None), } self.controls = [] self.sid = None self.announce_caps = True @log_calls('WhiteboardPlugin') def _update_caps(self, _account, features): if not self.announce_caps: return features.append(NS_JINGLE_SXE) features.append(NS_SXE) @log_calls('WhiteboardPlugin') def activate(self): if not HAS_GOOCANVAS: raise GajimPluginException('python-pygoocanvas is missing!') self.announce_caps = True for con in app.connections.values(): con.get_module('Caps').update_caps() @log_calls('WhiteboardPlugin') def deactivate(self): self.announce_caps = False for con in app.connections.values(): con.get_module('Caps').update_caps() @log_calls('WhiteboardPlugin') def connect_with_chat_control(self, control): for base in self.controls: if base.chat_control == control: self.controls.remove(base) if control.is_chat: base = Base(self, control) self.controls.append(base) self.update_button_state(control) @log_calls('WhiteboardPlugin') def disconnect_from_chat_control(self, chat_control): for base in self.controls: base.disconnect_from_chat_control() self.controls = [] @log_calls('WhiteboardPlugin') def update_button_state(self, control): for base in self.controls: if base.chat_control == control: if (control.contact.supports(NS_JINGLE_SXE) and control.contact.supports(NS_SXE)): base.enable_action(True) else: base.enable_action(False) @log_calls('WhiteboardPlugin') def show_request_dialog(self, account, fjid, jid, sid, content_types): def _on_accept(): session = app.connections[account].get_module( 'Jingle').get_jingle_session(fjid, sid) self.sid = session.sid if not session.accepted: session.approve_session() for content in content_types: session.approve_content('xhtml') for _jid in (fjid, jid): ctrl = app.window.get_control(account, _jid) if ctrl: break if not ctrl: # Create it app.interface.start_chat_from_jid(account, jid) ctrl = app.window.get_control(account, jid) session = session.contents[('initiator', 'xhtml')] ctrl.draw_whiteboard(session) def _on_decline(): session = app.connections[account].get_module( 'Jingle').get_jingle_session(fjid, sid) session.decline_session() client = app.get_client(account) contact = client.get_module('Contacts').get_contact(jid) ConfirmationDialog( _('Incoming Whiteboard'), _('Incoming Whiteboard Request'), _('%(name)s (%(jid)s) wants to start a whiteboard with ' 'you.') % {'name': contact.name, 'jid': jid}, [DialogButton.make('Cancel', text=_('_Decline'), callback=_on_decline), DialogButton.make('OK', text=_('_Accept'), callback=_on_accept)], transient_for=app.window).show() @log_calls('WhiteboardPlugin') def _nec_jingle_received(self, obj): if not HAS_GOOCANVAS: return content_types = obj.contents.media if content_types != 'xhtml': return self.show_request_dialog( obj.conn.name, obj.fjid, obj.jid, obj.sid, content_types) @log_calls('WhiteboardPlugin') def _nec_jingle_connected(self, obj): if not HAS_GOOCANVAS: return account = obj.conn.name ctrl = app.window.get_control(account, obj.jid) if not ctrl: return session = app.connections[obj.conn.name].get_module( 'Jingle').get_jingle_session(obj.fjid, obj.sid) if ('initiator', 'xhtml') not in session.contents: return session = session.contents[('initiator', 'xhtml')] ctrl.draw_whiteboard(session) @log_calls('WhiteboardPlugin') def _nec_jingle_disconnected(self, obj): for base in self.controls: if base.sid == obj.sid: base.stop_whiteboard(reason=obj.reason) @log_calls('WhiteboardPlugin') def _nec_raw_message(self, obj): if not HAS_GOOCANVAS: return if obj.stanza.getTag('sxe', namespace=NS_SXE): account = obj.conn.name try: fjid = helpers.get_full_jid_from_iq(obj.stanza) except helpers.InvalidFormat: obj.conn.dispatch('ERROR', (_('Invalid XMPP Address'), _('A message from a non-valid XMPP address ' 'arrived. It has been ignored.'))) jid = app.get_jid_without_resource(fjid) ctrl = app.window.get_control(account, jid) if not ctrl: return sxe = obj.stanza.getTag('sxe') if not sxe: return sid = sxe.getAttr('session') if (jid, sid) not in obj.conn.get_module('Jingle')._sessions: pass # newjingle = JingleSession(con=self, weinitiate=False, jid=jid, # sid=sid) # self.addJingle(newjingle) # We already have such session in dispatcher session = obj.conn.get_module('Jingle').get_jingle_session(fjid, sid) cn = session.contents[('initiator', 'xhtml')] error = obj.stanza.getTag('error') if error: action = 'iq-error' else: action = 'edit' cn.on_stanza(obj.stanza, sxe, error, action) # def __editCB(self, stanza, content, error, action): # new_tags = sxe.getTags('new') # remove_tags = sxe.getTags('remove') # if new_tags is not None: # # Process new elements # for tag in new_tags: # if tag.getAttr('type') == 'element': # ctrl.whiteboard.recieve_element(tag) # elif tag.getAttr('type') == 'attr': # ctrl.whiteboard.recieve_attr(tag) # ctrl.whiteboard.apply_new() # if remove_tags is not None: # # Delete rids # for tag in remove_tags: # target = tag.getAttr('target') # ctrl.whiteboard.image.del_rid(target) # Stop propagating this event, it's handled return True class Base(object): def __init__(self, plugin, chat_control): self.plugin = plugin self.chat_control = chat_control self.chat_control.draw_whiteboard = self.draw_whiteboard self.contact = self.chat_control.contact self.account = self.chat_control.account self.jid = self.contact.jid self.add_action() self.whiteboard = None self.sid = None def add_action(self): action_name = 'toggle-whiteboard-' + self.chat_control.control_id act = Gio.SimpleAction.new_stateful( action_name, None, GLib.Variant.new_boolean(False)) act.connect('change-state', self.on_whiteboard_button_toggled) app.window.add_action(act) self.chat_control.control_menu.append( 'WhiteBoard', 'win.' + action_name) def enable_action(self, state): action_name = 'toggle-whiteboard-' + self.chat_control.control_id app.window.lookup_action(action_name).set_enabled(state) def draw_whiteboard(self, content): hbox = self.chat_control.xml.get_object('chat_control_hbox') if len(hbox.get_children()) == 1: self.whiteboard = Whiteboard(self.account, self.contact, content, self.plugin) # Set minimum size self.whiteboard.hbox.set_size_request(300, 0) hbox.pack_start(self.whiteboard.hbox, False, False, 0) self.whiteboard.hbox.show_all() self.enable_action(True) content.control = self self.sid = content.session.sid def on_whiteboard_button_toggled(self, action, param): """ Popup whiteboard """ action.set_state(param) state = param.get_boolean() if state: if not self.whiteboard: self.start_whiteboard() else: self.stop_whiteboard() def start_whiteboard(self): conn = app.connections[self.chat_control.account] jingle = JingleSession(conn, weinitiate=True, jid=self.jid) self.sid = jingle.sid conn.get_module('Jingle')._sessions[jingle.sid] = jingle content = JingleWhiteboard(jingle) content.control = self jingle.add_content('xhtml', content) jingle.start_session() def stop_whiteboard(self, reason=None): conn = app.connections[self.chat_control.account] self.sid = None session = conn.get_module('Jingle').get_jingle_session(self.jid, media='xhtml') if session: session.end_session() self.enable_action(False) if reason: txt = _('Whiteboard stopped: %(reason)s') % {'reason': reason} self.chat_control.add_info_message(txt) if not self.whiteboard: return hbox = self.chat_control.xml.get_object('chat_control_hbox') if self.whiteboard.hbox in hbox.get_children(): if hasattr(self.whiteboard, 'hbox'): hbox.remove(self.whiteboard.hbox) self.whiteboard = None def disconnect_from_chat_control(self): menu = self.chat_control.control_menu for i in range(menu.get_n_items()): label = menu.get_item_attribute_value(i, 'label') if label.get_string() == 'WhiteBoard': menu.remove(i) break class JingleWhiteboard(JingleContent): ''' Jingle Whiteboard sessions consist of xhtml content''' def __init__(self, session, transport=None, senders=None): if not transport: transport = JingleTransportSXE() JingleContent.__init__(self, session, transport, senders) self.media = 'xhtml' self.negotiated = True # There is nothing to negotiate self.last_rid = 0 self.callbacks['session-accept'] += [self._sessionAcceptCB] self.callbacks['session-terminate'] += [self._stop] self.callbacks['session-terminate-sent'] += [self._stop] self.callbacks['edit'] = [self._EditCB] @log_calls('WhiteboardPlugin') def _EditCB(self, stanza, content, error, action): new_tags = content.getTags('new') remove_tags = content.getTags('remove') if not self.control.whiteboard: return if new_tags is not None: # Process new elements for tag in new_tags: if tag.getAttr('type') == 'element': self.control.whiteboard.recieve_element(tag) elif tag.getAttr('type') == 'attr': self.control.whiteboard.recieve_attr(tag) self.control.whiteboard.apply_new() if remove_tags is not None: # Delete rids for tag in remove_tags: target = tag.getAttr('target') self.control.whiteboard.image.del_rid(target) @log_calls('WhiteboardPlugin') def _sessionAcceptCB(self, stanza, content, error, action): log.debug('session accepted') self.session.connection.dispatch( 'WHITEBOARD_ACCEPTED', (self.session.peerjid, self.session.sid)) def generate_rids(self, x): # Generates x number of rids and returns in list rids = [] for x in range(x): rids.append(str(self.last_rid)) self.last_rid += 1 return rids @log_calls('WhiteboardPlugin') def send_whiteboard_node(self, items, rids): # Takes int rid and dict items and sends it as a node # sends new item jid = self.session.peerjid sid = self.session.sid message = Message(to=jid) sxe = message.addChild(name='sxe', attrs={'session': sid}, namespace=NS_SXE) for x in rids: if items[x]['type'] == 'element': parent = x attrs = {'rid': x, 'name': items[x]['data'][0].getName(), 'type': items[x]['type']} sxe.addChild(name='new', attrs=attrs) if items[x]['type'] == 'attr': attr_name = items[x]['data'] chdata = items[parent]['data'][0].getAttr(attr_name) attrs = {'rid': x, 'name': attr_name, 'type': items[x]['type'], 'chdata': chdata, 'parent': parent} sxe.addChild(name='new', attrs=attrs) self.session.connection.connection.send(message) @log_calls('WhiteboardPlugin') def delete_whiteboard_node(self, rids): message = Message(to=self.session.peerjid) sxe = message.addChild(name='sxe', attrs={'session': self.session.sid}, namespace=NS_SXE) for x in rids: sxe.addChild(name='remove', attrs={'target': x}) self.session.connection.connection.send(message) def send_items(self, items, rids): # Receives dict items and a list of rids of items to send # TODO: Is there a less clumsy way that doesn't involve passing # whole list? self.send_whiteboard_node(items, rids) def del_item(self, rids): self.delete_whiteboard_node(rids) def encode(self, xml): # Encodes it sendable string return 'data:text/xml,' + quote(xml) def _fill_content(self, content): content.addChild(NS_JINGLE_XHTML + ' description') def _stop(self, *things): pass def __del__(self): pass def get_content(desc): return JingleWhiteboard common.jingle_content.contents[NS_JINGLE_XHTML] = get_content class JingleTransportSXE(JingleTransport): def __init__(self, node=None): JingleTransport.__init__(self, TransportType.SOCKS5) def make_transport(self, candidates=None): transport = JingleTransport.make_transport(self, candidates) transport.setNamespace(NS_JINGLE_SXE) transport.setTagData('host', 'TODO') return transport common.jingle_transport.transports[NS_JINGLE_SXE] = JingleTransportSXE