diff --git a/gajim/adhoc_commands.py b/gajim/adhoc_commands.py index 87e439712..0db00eeee 100644 --- a/gajim/adhoc_commands.py +++ b/gajim/adhoc_commands.py @@ -25,12 +25,12 @@ # FIXME: think if we need caching command list. it may be wrong if there will # be entities that often change the list, it may be slow to fetch it every time -from gi.repository import GLib from gi.repository import Gtk import nbxmpp from gajim.common import app from gajim.common import dataforms +from gajim.common import ged from gajim import gtkgui_helpers from gajim import dialogs @@ -55,7 +55,7 @@ class CommandWindow: """ # an account object - self.account = app.connections[account] + self._con = app.connections[account] self.jid = jid self.commandnode = commandnode self.data_form_widget = None @@ -87,6 +87,14 @@ class CommandWindow: column = Gtk.TreeViewColumn("Command", renderer, text=0) self.command_treeview.append_column(column) + app.ged.register_event_handler( + 'adhoc-command-error', ged.CORE, self._on_command_error) + app.ged.register_event_handler( + 'adhoc-command-list', ged.CORE, self._on_command_list) + app.ged.register_event_handler('adhoc-command-action-response', + ged.CORE, + self._on_action_response) + self.initiate() def initiate(self): @@ -157,8 +165,13 @@ class CommandWindow: return False def on_adhoc_commands_window_destroy(self, *anything): - # TODO: do all actions that are needed to remove this object from memory - pass + app.ged.remove_event_handler( + 'adhoc-command-error', ged.CORE, self._on_command_error) + app.ged.remove_event_handler( + 'adhoc-command-list', ged.CORE, self._on_command_list) + app.ged.remove_event_handler('adhoc-command-action-response', + ged.CORE, + self._on_action_response) def on_adhoc_commands_window_delete_event(self, *anything): if self.stage_window_delete_cb: @@ -190,7 +203,7 @@ class CommandWindow: self.finish_button.set_sensitive(False) # request command list - self.request_command_list() + self._con.get_module('AdHocCommands').request_command_list(self.jid) self.retrieving_commands_spinner.start() # setup the callbacks @@ -304,7 +317,8 @@ class CommandWindow: return def on_yes(button): - self.send_cancel() + self._con.get_module('AdHocCommands').send_cancel( + self.jid, self.commandnode, self.sessionid) dialog.destroy() cb() @@ -371,7 +385,9 @@ class CommandWindow: self.finish_button.set_sensitive(False) self.sending_form_spinner.start() - self.send_command(action) + self._con.get_module('AdHocCommands').send_command( + self.jid, self.commandnode, self.sessionid, + self.data_form_widget.data_form, action) def stage3_next_form(self, command): if not isinstance(command, nbxmpp.Node): @@ -527,85 +543,15 @@ class CommandWindow: def stage5_restart_button_clicked(self, widget): self.restart() -# handling xml stanzas - def request_command_list(self): - """ - Request the command list. Change stage on delivery - """ - query = nbxmpp.Iq(typ='get', to=nbxmpp.JID(self.jid), - queryNS=nbxmpp.NS_DISCO_ITEMS) - query.setQuerynode(nbxmpp.NS_COMMANDS) + def _on_command_error(self, obj): + self.stage5(errorid=obj.error) - def callback(response): - '''Called on response to query.''' - # FIXME: move to connection_handlers.py - # is error => error stage - error = response.getError() - if error: - # extracting error description - self.stage5(errorid=error) - return - - # no commands => no commands stage - # commands => command selection stage - query = response.getTag('query') - if query and query.getAttr('node') == nbxmpp.NS_COMMANDS: - items = query.getTags('item') - else: - items = [] - if len(items)==0: - self.commandlist = [] - self.stage4() - else: - self.commandlist = [(t.getAttr('node'), t.getAttr('name')) \ - for t in items] - self.stage2() - - self.account.connection.SendAndCallForResponse(query, callback) - - def send_command(self, action='execute'): - """ - Send the command with data form. Wait for reply - """ - # create the stanza - assert action in ('execute', 'prev', 'next', 'complete') - - stanza = nbxmpp.Iq(typ='set', to=self.jid) - cmdnode = stanza.addChild('command', namespace=nbxmpp.NS_COMMANDS, - attrs={'node':self.commandnode, 'action':action}) - - if self.sessionid: - cmdnode.setAttr('sessionid', self.sessionid) - - if self.data_form_widget.data_form: - cmdnode.addChild(node=self.data_form_widget.data_form.get_purged()) - - def callback(response): - # FIXME: move to connection_handlers.py - err = response.getError() - if err: - self.stage5(errorid = err) - else: - self.stage3_next_form(response.getTag('command')) - - self.account.connection.SendAndCallForResponse(stanza, callback) - - def send_cancel(self): - """ - Send the command with action='cancel' - """ - assert self.commandnode - if self.sessionid and self.account.connection: - # we already have sessionid, so the service sent at least one reply. - stanza = nbxmpp.Iq(typ='set', to=self.jid) - stanza.addChild('command', namespace=nbxmpp.NS_COMMANDS, attrs={ - 'node':self.commandnode, - 'sessionid':self.sessionid, - 'action':'cancel' - }) - - self.account.connection.send(stanza) + def _on_command_list(self, obj): + self.commandlist = obj.commandlist + if not self.commandlist: + self.stage4() else: - # we did not received any reply from service; - # FIXME: we should wait and then send cancel; for now we do nothing - pass + self.stage2() + + def _on_action_response(self, obj): + self.stage3_next_form(obj.command) diff --git a/gajim/common/commands.py b/gajim/common/commands.py deleted file mode 100644 index a190f9b26..000000000 --- a/gajim/common/commands.py +++ /dev/null @@ -1,433 +0,0 @@ -# -*- coding:utf-8 -*- -## src/common/commands.py -## -## Copyright (C) 2006-2014 Yann Leboulanger -## Copyright (C) 2006-2007 Tomasz Melcer -## Copyright (C) 2007 Jean-Marie Traissard -## Copyright (C) 2008 Brendan Taylor -## Stephan Erb -## -## This file is part of Gajim. -## -## 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 . -## - -import nbxmpp -from gajim.common import helpers -from gajim.common import dataforms -from gajim.common import app - -import logging -log = logging.getLogger('gajim.c.commands') - -class AdHocCommand: - commandnode = 'command' - commandname = 'The Command' - commandfeatures = (nbxmpp.NS_DATA,) - - @staticmethod - def isVisibleFor(samejid): - """ - This returns True if that command should be visible and invokable for - others - - samejid - True when command is invoked by an entity with the same bare - jid. - """ - return True - - def __init__(self, conn, jid, sessionid): - self.connection = conn - self.jid = jid - self.sessionid = sessionid - - def buildResponse(self, request, status = 'executing', defaultaction = None, - actions = None): - assert status in ('executing', 'completed', 'canceled') - - response = request.buildReply('result') - cmd = response.getTag('command', namespace=nbxmpp.NS_COMMANDS) - cmd.setAttr('sessionid', self.sessionid) - cmd.setAttr('node', self.commandnode) - cmd.setAttr('status', status) - if defaultaction is not None or actions is not None: - if defaultaction is not None: - assert defaultaction in ('cancel', 'execute', 'prev', 'next', - 'complete') - attrs = {'action': defaultaction} - else: - attrs = {} - - cmd.addChild('actions', attrs, actions) - return response, cmd - - def badRequest(self, stanza): - self.connection.connection.send(nbxmpp.Error(stanza, - nbxmpp.NS_STANZAS + ' bad-request')) - - def cancel(self, request): - response = self.buildResponse(request, status = 'canceled')[0] - self.connection.connection.send(response) - return False # finish the session - -class ChangeStatusCommand(AdHocCommand): - commandnode = 'change-status' - commandname = _('Change status information') - - def __init__(self, conn, jid, sessionid): - AdHocCommand.__init__(self, conn, jid, sessionid) - self.cb = self.first_step - - @staticmethod - def isVisibleFor(samejid): - """ - Change status is visible only if the entity has the same bare jid - """ - return samejid - - def execute(self, request): - return self.cb(request) - - def first_step(self, request): - # first query... - response, cmd = self.buildResponse(request, defaultaction = 'execute', - actions = ['execute']) - - cmd.addChild(node = dataforms.SimpleDataForm( - title = _('Change status'), - instructions = _('Set the presence type and description'), - fields = [ - dataforms.Field('list-single', - var = 'presence-type', - label = 'Type of presence:', - options = [ - ('chat', _('Free for chat')), - ('online', _('Online')), - ('away', _('Away')), - ('xa', _('Extended away')), - ('dnd', _('Do not disturb')), - ('offline', _('Offline - disconnect'))], - value = 'online', - required = True), - dataforms.Field('text-multi', - var = 'presence-desc', - label = _('Presence description:'))])) - - self.connection.connection.send(response) - - # for next invocation - self.cb = self.second_step - - return True # keep the session - - def second_step(self, request): - # check if the data is correct - try: - form = dataforms.SimpleDataForm(extend = request.getTag('command').\ - getTag('x')) - except Exception: - self.badRequest(request) - return False - - try: - presencetype = form['presence-type'].value - if not presencetype in \ - ('chat', 'online', 'away', 'xa', 'dnd', 'offline'): - self.badRequest(request) - return False - except Exception: # KeyError if there's no presence-type field in form or - # AttributeError if that field is of wrong type - self.badRequest(request) - return False - - try: - presencedesc = form['presence-desc'].value - except Exception: # same exceptions as in last comment - presencedesc = '' - - response, cmd = self.buildResponse(request, status = 'completed') - cmd.addChild('note', {}, _('The status has been changed.')) - - # if going offline, we need to push response so it won't go into - # queue and disappear - self.connection.connection.send(response, now = presencetype == 'offline') - - # send new status - app.interface.roster.send_status(self.connection.name, presencetype, - presencedesc) - - return False # finish the session - -def find_current_groupchats(account): - from gajim import message_control - rooms = [] - for gc_control in app.interface.msg_win_mgr.get_controls( - message_control.TYPE_GC) + \ - app.interface.minimized_controls[account].values(): - acct = gc_control.account - # check if account is the good one - if acct != account: - continue - room_jid = gc_control.room_jid - nick = gc_control.nick - if room_jid in app.gc_connected[acct] and \ - app.gc_connected[acct][room_jid]: - rooms.append((room_jid, nick,)) - return rooms - - -class LeaveGroupchatsCommand(AdHocCommand): - commandnode = 'leave-groupchats' - commandname = _('Leave Groupchats') - - def __init__(self, conn, jid, sessionid): - AdHocCommand.__init__(self, conn, jid, sessionid) - self.cb = self.first_step - - @staticmethod - def isVisibleFor(samejid): - """ - Leave groupchats is visible only if the entity has the same bare jid - """ - return samejid - - def execute(self, request): - return self.cb(request) - - def first_step(self, request): - # first query... - response, cmd = self.buildResponse(request, defaultaction = 'execute', - actions=['execute']) - options = [] - account = self.connection.name - for gc in find_current_groupchats(account): - options.append(('%s' %(gc[0]), _('%(nickname)s on %(room_jid)s') % \ - {'nickname': gc[1], 'room_jid': gc[0]})) - if not len(options): - response, cmd = self.buildResponse(request, status = 'completed') - cmd.addChild('note', {}, _('You have not joined a groupchat.')) - - self.connection.connection.send(response) - return False - - cmd.addChild(node=dataforms.SimpleDataForm( - title = _('Leave Groupchats'), - instructions = _('Choose the groupchats you want to leave'), - fields=[ - dataforms.Field('list-multi', - var = 'groupchats', - label = _('Groupchats'), - options = options, - required = True)])) - - self.connection.connection.send(response) - - # for next invocation - self.cb = self.second_step - - return True # keep the session - - def second_step(self, request): - # check if the data is correct - try: - form = dataforms.SimpleDataForm(extend = request.getTag('command').\ - getTag('x')) - except Exception: - self.badRequest(request) - return False - - try: - gc = form['groupchats'].values - except Exception: # KeyError if there's no groupchats in form - self.badRequest(request) - return False - account = self.connection.name - try: - for room_jid in gc: - gc_control = app.interface.msg_win_mgr.get_gc_control(room_jid, - account) - if not gc_control: - gc_control = app.interface.minimized_controls[account]\ - [room_jid] - gc_control.shutdown() - app.interface.roster.remove_groupchat(room_jid, account) - continue - gc_control.parent_win.remove_tab(gc_control, None, force = True) - except Exception: # KeyError if there's no such room opened - self.badRequest(request) - return False - response, cmd = self.buildResponse(request, status = 'completed') - note = _('You left the following groupchats:') - for room_jid in gc: - note += '\n\t' + room_jid - cmd.addChild('note', {}, note) - - self.connection.connection.send(response) - return False - - -class ConnectionCommands: - """ - This class depends on that it is a part of Connection() class - """ - - def __init__(self): - # a list of all commands exposed: node -> command class - self.__commands = {} - if app.config.get('remote_commands'): - for cmdobj in (ChangeStatusCommand, LeaveGroupchatsCommand): - self.__commands[cmdobj.commandnode] = cmdobj - - # a list of sessions; keys are tuples (jid, sessionid, node) - self.__sessions = {} - - def getOurBareJID(self): - return app.get_jid_from_account(self.name) - - def isSameJID(self, jid): - """ - Test if the bare jid given is the same as our bare jid - """ - return nbxmpp.JID(jid).getStripped() == self.getOurBareJID() - - def commandListQuery(self, con, iq_obj): - iq = iq_obj.buildReply('result') - jid = helpers.get_full_jid_from_iq(iq_obj) - q = iq.getTag('query') - # buildReply don't copy the node attribute. Re-add it - q.setAttr('node', nbxmpp.NS_COMMANDS) - - for node, cmd in self.__commands.items(): - if cmd.isVisibleFor(self.isSameJID(jid)): - q.addChild('item', { - # TODO: find the jid - 'jid': self.getOurBareJID() + '/' + self.server_resource, - 'node': node, - 'name': cmd.commandname}) - - self.connection.send(iq) - - def commandInfoQuery(self, con, iq_obj): - """ - Send disco#info result for query for command (JEP-0050, example 6.). - Return True if the result was sent, False if not - """ - try: - jid = helpers.get_full_jid_from_iq(iq_obj) - except helpers.InvalidFormat: - log.warning('Invalid JID: %s, ignoring it' % iq_obj.getFrom()) - return - node = iq_obj.getTagAttr('query', 'node') - - if node not in self.__commands: return False - - cmd = self.__commands[node] - if cmd.isVisibleFor(self.isSameJID(jid)): - iq = iq_obj.buildReply('result') - q = iq.getTag('query') - q.addChild('identity', attrs = {'type': 'command-node', - 'category': 'automation', - 'name': cmd.commandname}) - q.addChild('feature', attrs = {'var': nbxmpp.NS_COMMANDS}) - for feature in cmd.commandfeatures: - q.addChild('feature', attrs = {'var': feature}) - - self.connection.send(iq) - return True - - return False - - def commandItemsQuery(self, con, iq_obj): - """ - Send disco#items result for query for command. Return True if the result - was sent, False if not. - """ - jid = helpers.get_full_jid_from_iq(iq_obj) - node = iq_obj.getTagAttr('query', 'node') - - if node not in self.__commands: return False - - cmd = self.__commands[node] - if cmd.isVisibleFor(self.isSameJID(jid)): - iq = iq_obj.buildReply('result') - self.connection.send(iq) - return True - - return False - - def _CommandExecuteCB(self, con, iq_obj): - jid = helpers.get_full_jid_from_iq(iq_obj) - - cmd = iq_obj.getTag('command') - if cmd is None: return - - node = cmd.getAttr('node') - if node is None: return - - sessionid = cmd.getAttr('sessionid') - if sessionid is None: - # we start a new command session... only if we are visible for the jid - # and command exist - if node not in self.__commands.keys(): - self.connection.send( - nbxmpp.Error(iq_obj, nbxmpp.NS_STANZAS + ' item-not-found')) - raise nbxmpp.NodeProcessed - - newcmd = self.__commands[node] - if not newcmd.isVisibleFor(self.isSameJID(jid)): - return - - # generate new sessionid - sessionid = self.connection.getAnID() - - # create new instance and run it - obj = newcmd(conn = self, jid = jid, sessionid = sessionid) - rc = obj.execute(iq_obj) - if rc: - self.__sessions[(jid, sessionid, node)] = obj - raise nbxmpp.NodeProcessed - else: - # the command is already running, check for it - magictuple = (jid, sessionid, node) - if magictuple not in self.__sessions: - # we don't have this session... ha! - return - - action = cmd.getAttr('action') - obj = self.__sessions[magictuple] - - try: - if action == 'cancel': - rc = obj.cancel(iq_obj) - elif action == 'prev': - rc = obj.prev(iq_obj) - elif action == 'next': - rc = obj.next(iq_obj) - elif action == 'execute' or action is None: - rc = obj.execute(iq_obj) - elif action == 'complete': - rc = obj.complete(iq_obj) - else: - # action is wrong. stop the session, send error - raise AttributeError - except AttributeError: - # the command probably doesn't handle invoked action... - # stop the session, return error - del self.__sessions[magictuple] - return - - # delete the session if rc is False - if not rc: - del self.__sessions[magictuple] - - raise nbxmpp.NodeProcessed diff --git a/gajim/common/connection_handlers.py b/gajim/common/connection_handlers.py index 85fb25b2c..e1a2f6773 100644 --- a/gajim/common/connection_handlers.py +++ b/gajim/common/connection_handlers.py @@ -42,7 +42,6 @@ from gajim.common import helpers from gajim.common import app from gajim.common import jingle_xtls from gajim.common.caps_cache import muc_caps_cache -from gajim.common.commands import ConnectionCommands from gajim.common.protocol.caps import ConnectionCaps from gajim.common.protocol.bytestream import ConnectionSocks5Bytestream from gajim.common.protocol.bytestream import ConnectionIBBytestream @@ -218,7 +217,7 @@ class ConnectionDisco: if not self.connection or self.connected < 2: return - if self.commandItemsQuery(con, iq_obj): + if self.get_module('AdHocCommands').command_items_query(iq_obj): raise nbxmpp.NodeProcessed node = iq_obj.getTagAttr('query', 'node') if node is None: @@ -226,7 +225,7 @@ class ConnectionDisco: self.connection.send(result) raise nbxmpp.NodeProcessed if node == nbxmpp.NS_COMMANDS: - self.commandListQuery(con, iq_obj) + self.get_module('AdHocCommands').command_list_query(iq_obj) raise nbxmpp.NodeProcessed def _DiscoverInfoGetCB(self, con, iq_obj): @@ -235,7 +234,7 @@ class ConnectionDisco: return node = iq_obj.getQuerynode() - if self.commandInfoQuery(con, iq_obj): + if self.get_module('AdHocCommands').command_info_query(iq_obj): raise nbxmpp.NodeProcessed id_ = iq_obj.getAttr('id') @@ -743,14 +742,12 @@ class ConnectionHandlersBase: return sess class ConnectionHandlers(ConnectionArchive313, -ConnectionSocks5Bytestream, ConnectionDisco, -ConnectionCommands, ConnectionCaps, +ConnectionSocks5Bytestream, ConnectionDisco, ConnectionCaps, ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream): def __init__(self): ConnectionArchive313.__init__(self) ConnectionSocks5Bytestream.__init__(self) ConnectionIBBytestream.__init__(self) - ConnectionCommands.__init__(self) # Handle presences BEFORE caps app.nec.register_incoming_event(PresenceReceivedEvent) @@ -1339,8 +1336,6 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream): nbxmpp.NS_MUC_ADMIN) con.RegisterHandler('iq', self._SecLabelCB, 'result', nbxmpp.NS_SECLABEL_CATALOG) - con.RegisterHandler('iq', self._CommandExecuteCB, 'set', - nbxmpp.NS_COMMANDS) con.RegisterHandler('iq', self._DiscoverInfoGetCB, 'get', nbxmpp.NS_DISCO_INFO) con.RegisterHandler('iq', self._DiscoverItemsGetCB, 'get', diff --git a/gajim/common/modules/__init__.py b/gajim/common/modules/__init__.py index e3b395f16..92f084d3d 100644 --- a/gajim/common/modules/__init__.py +++ b/gajim/common/modules/__init__.py @@ -18,6 +18,8 @@ from pathlib import Path log = logging.getLogger('gajim.c.m') +ZEROCONF_MODULES = ['adhoc_commands'] + imported_modules = [] _modules = {} @@ -31,12 +33,26 @@ for file in Path(__file__).parent.iterdir(): if file.stem == 'pep': # Register the PEP module first, because other modules # depend on it - imported_modules.insert(0, module) + imported_modules.insert(0, (module, file.stem)) else: - imported_modules.append(module) + imported_modules.append((module, file.stem)) class ModuleMock: + def __init__(self, name): + self._name = name + + # HTTPUpload + self.available = False + + # Blocking + self.blocked = [] + + # Privacy Lists + self.blocked_contacts = [] + self.blocked_groups = [] + self.blocked_all = False + def __getattr__(self, key): def _mock(self, *args, **kwargs): return @@ -48,7 +64,11 @@ def register(con, *args, **kwargs): return _modules[con.name] = {} for module in imported_modules: - instance, name = module.get_instance(con, *args, **kwargs) + mod, name = module + if con.name == 'Local': + if name not in ZEROCONF_MODULES: + continue + instance, name = mod.get_instance(con, *args, **kwargs) _modules[con.name][name] = instance @@ -60,7 +80,7 @@ def get(account, name): try: return _modules[account][name] except KeyError: - return ModuleMock() + return ModuleMock(name) def get_handlers(con): diff --git a/gajim/common/modules/adhoc_commands.py b/gajim/common/modules/adhoc_commands.py new file mode 100644 index 000000000..acbbb60db --- /dev/null +++ b/gajim/common/modules/adhoc_commands.py @@ -0,0 +1,573 @@ +# Copyright (C) 2006-2014 Yann Leboulanger +# Copyright (C) 2006-2007 Tomasz Melcer +# Copyright (C) 2007 Jean-Marie Traissard +# Copyright (C) 2008 Brendan Taylor +# Stephan Erb +# Copyright (C) 2018 Philipp Hörist +# +# This file is part of Gajim. +# +# 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 . + +import logging +import nbxmpp + +from gajim.common import helpers +from gajim.common import dataforms +from gajim.common import app +from gajim.common.nec import NetworkIncomingEvent + +log = logging.getLogger('gajim.c.m.commands') + + +class AdHocCommand: + commandnode = 'command' + commandname = 'The Command' + commandfeatures = (nbxmpp.NS_DATA,) + + @staticmethod + def isVisibleFor(samejid): + """ + This returns True if that command should be visible and invokable for + others + + samejid - True when command is invoked by an entity with the same bare + jid. + """ + return True + + def __init__(self, conn, jid, sessionid): + self.connection = conn + self.jid = jid + self.sessionid = sessionid + + def buildResponse(self, request, status='executing', defaultaction=None, + actions=None): + assert status in ('executing', 'completed', 'canceled') + + response = request.buildReply('result') + cmd = response.getTag('command', namespace=nbxmpp.NS_COMMANDS) + cmd.setAttr('sessionid', self.sessionid) + cmd.setAttr('node', self.commandnode) + cmd.setAttr('status', status) + if defaultaction is not None or actions is not None: + if defaultaction is not None: + assert defaultaction in ('cancel', 'execute', 'prev', 'next', + 'complete') + attrs = {'action': defaultaction} + else: + attrs = {} + + cmd.addChild('actions', attrs, actions) + return response, cmd + + def badRequest(self, stanza): + self.connection.connection.send( + nbxmpp.Error(stanza, nbxmpp.NS_STANZAS + ' bad-request')) + + def cancel(self, request): + response = self.buildResponse(request, status='canceled')[0] + self.connection.connection.send(response) + return False # finish the session + + +class ChangeStatusCommand(AdHocCommand): + commandnode = 'change-status' + commandname = _('Change status information') + + def __init__(self, conn, jid, sessionid): + AdHocCommand.__init__(self, conn, jid, sessionid) + self.cb = self.first_step + + @staticmethod + def isVisibleFor(samejid): + """ + Change status is visible only if the entity has the same bare jid + """ + return samejid + + def execute(self, request): + return self.cb(request) + + def first_step(self, request): + # first query... + response, cmd = self.buildResponse(request, + defaultaction='execute', + actions=['execute']) + + cmd.addChild( + node=dataforms.SimpleDataForm( + title=_('Change status'), + instructions=_('Set the presence type and description'), + fields=[ + dataforms.Field( + 'list-single', + var='presence-type', + label='Type of presence:', + options=[ + ('chat', _('Free for chat')), + ('online', _('Online')), + ('away', _('Away')), + ('xa', _('Extended away')), + ('dnd', _('Do not disturb')), + ('offline', _('Offline - disconnect'))], + value='online', + required=True), + dataforms.Field( + 'text-multi', + var='presence-desc', + label=_('Presence description:')) + ] + ) + ) + + self.connection.connection.send(response) + + # for next invocation + self.cb = self.second_step + + return True # keep the session + + def second_step(self, request): + # check if the data is correct + try: + form = dataforms.SimpleDataForm( + extend=request.getTag('command').getTag('x')) + except Exception: + self.badRequest(request) + return False + + try: + presencetype = form['presence-type'].value + if presencetype not in ('chat', 'online', 'away', + 'xa', 'dnd', 'offline'): + self.badRequest(request) + return False + except Exception: + # KeyError if there's no presence-type field in form or + # AttributeError if that field is of wrong type + self.badRequest(request) + return False + + try: + presencedesc = form['presence-desc'].value + except Exception: # same exceptions as in last comment + presencedesc = '' + + response, cmd = self.buildResponse(request, status='completed') + cmd.addChild('note', {}, _('The status has been changed.')) + + # if going offline, we need to push response so it won't go into + # queue and disappear + self.connection.connection.send(response, + now=presencetype == 'offline') + + # send new status + app.interface.roster.send_status( + self.connection.name, presencetype, presencedesc) + + return False # finish the session + + +def find_current_groupchats(account): + from gajim import message_control + rooms = [] + for gc_control in app.interface.msg_win_mgr.get_controls( + message_control.TYPE_GC) + \ + app.interface.minimized_controls[account].values(): + acct = gc_control.account + # check if account is the good one + if acct != account: + continue + room_jid = gc_control.room_jid + nick = gc_control.nick + if (room_jid in app.gc_connected[acct] and + app.gc_connected[acct][room_jid]): + rooms.append((room_jid, nick,)) + return rooms + + +class LeaveGroupchatsCommand(AdHocCommand): + commandnode = 'leave-groupchats' + commandname = _('Leave Groupchats') + + def __init__(self, conn, jid, sessionid): + AdHocCommand.__init__(self, conn, jid, sessionid) + self.cb = self.first_step + + @staticmethod + def isVisibleFor(samejid): + """ + Leave groupchats is visible only if the entity has the same bare jid + """ + return samejid + + def execute(self, request): + return self.cb(request) + + def first_step(self, request): + # first query... + response, cmd = self.buildResponse(request, + defaultaction='execute', + actions=['execute']) + options = [] + account = self.connection.name + for gc in find_current_groupchats(account): + options.append( + ('%s' % gc[0], + _('%(nickname)s on %(room_jid)s') % {'nickname': gc[1], + 'room_jid': gc[0]})) + if not len(options): + response, cmd = self.buildResponse(request, status='completed') + cmd.addChild('note', {}, _('You have not joined a groupchat.')) + + self.connection.connection.send(response) + return False + + cmd.addChild( + node=dataforms.SimpleDataForm( + title=_('Leave Groupchats'), + instructions=_('Choose the groupchats you want to leave'), + fields=[ + dataforms.Field( + 'list-multi', + var='groupchats', + label=_('Groupchats'), + options=options, + required=True) + ] + ) + ) + + self.connection.connection.send(response) + + # for next invocation + self.cb = self.second_step + + return True # keep the session + + def second_step(self, request): + # check if the data is correct + try: + form = dataforms.SimpleDataForm( + extend=request.getTag('command').getTag('x')) + except Exception: + self.badRequest(request) + return False + + try: + gc = form['groupchats'].values + except Exception: # KeyError if there's no groupchats in form + self.badRequest(request) + return False + account = self.connection.name + try: + for room_jid in gc: + gc_control = app.interface.msg_win_mgr.get_gc_control( + room_jid, account) + if not gc_control: + gc_control = app.interface.minimized_controls[account][room_jid] + gc_control.shutdown() + app.interface.roster.remove_groupchat(room_jid, account) + continue + gc_control.parent_win.remove_tab(gc_control, None, force=True) + except Exception: # KeyError if there's no such room opened + self.badRequest(request) + return False + response, cmd = self.buildResponse(request, status='completed') + note = _('You left the following groupchats:') + for room_jid in gc: + note += '\n\t' + room_jid + cmd.addChild('note', {}, note) + + self.connection.connection.send(response) + return False + + +class AdHocCommands: + def __init__(self, con): + self._con = con + self._account = con.name + + self.handlers = [ + ('iq', self._execute_command_received, 'set', nbxmpp.NS_COMMANDS) + ] + + # a list of all commands exposed: node -> command class + self._commands = {} + if app.config.get('remote_commands'): + for cmdobj in (ChangeStatusCommand, LeaveGroupchatsCommand): + self._commands[cmdobj.commandnode] = cmdobj + + # a list of sessions; keys are tuples (jid, sessionid, node) + self._sessions = {} + + def get_own_bare_jid(self): + return self._con.get_own_jid().getStripped() + + def is_same_jid(self, jid): + """ + Test if the bare jid given is the same as our bare jid + """ + return nbxmpp.JID(jid).getStripped() == self.get_own_bare_jid() + + def command_list_query(self, stanza): + iq = stanza.buildReply('result') + jid = helpers.get_full_jid_from_iq(stanza) + q = iq.getTag('query') + # buildReply don't copy the node attribute. Re-add it + q.setAttr('node', nbxmpp.NS_COMMANDS) + + for node, cmd in self._commands.items(): + if cmd.isVisibleFor(self.is_same_jid(jid)): + q.addChild('item', { + # TODO: find the jid + 'jid': str(self._con.get_own_jid()), + 'node': node, + 'name': cmd.commandname}) + + self._con.connection.send(iq) + + def command_info_query(self, stanza): + """ + Send disco#info result for query for command (XEP-0050, example 6.). + Return True if the result was sent, False if not + """ + try: + jid = helpers.get_full_jid_from_iq(stanza) + except helpers.InvalidFormat: + log.warning('Invalid JID: %s, ignoring it' % stanza.getFrom()) + return + node = stanza.getTagAttr('query', 'node') + + if node not in self._commands: + return False + + cmd = self._commands[node] + if cmd.isVisibleFor(self.is_same_jid(jid)): + iq = stanza.buildReply('result') + q = iq.getTag('query') + q.addChild('identity', + attrs={'type': 'command-node', + 'category': 'automation', + 'name': cmd.commandname}) + q.addChild('feature', attrs={'var': nbxmpp.NS_COMMANDS}) + for feature in cmd.commandfeatures: + q.addChild('feature', attrs={'var': feature}) + + self._con.connection.send(iq) + return True + + return False + + def command_items_query(self, stanza): + """ + Send disco#items result for query for command. + Return True if the result was sent, False if not. + """ + jid = helpers.get_full_jid_from_iq(stanza) + node = stanza.getTagAttr('query', 'node') + + if node not in self._commands: + return False + + cmd = self._commands[node] + if cmd.isVisibleFor(self.is_same_jid(jid)): + iq = stanza.buildReply('result') + self._con.connection.send(iq) + return True + + return False + + def _execute_command_received(self, con, stanza): + jid = helpers.get_full_jid_from_iq(stanza) + + cmd = stanza.getTag('command') + if cmd is None: + log.error('Malformed stanza (no command node) %s', stanza) + raise nbxmpp.NodeProcessed + + node = cmd.getAttr('node') + if node is None: + log.error('Malformed stanza (no node attr) %s', stanza) + raise nbxmpp.NodeProcessed + + sessionid = cmd.getAttr('sessionid') + if sessionid is None: + # we start a new command session + # only if we are visible for the jid and command exist + if node not in self._commands.keys(): + self._con.connection.send( + nbxmpp.Error( + stanza, nbxmpp.NS_STANZAS + ' item-not-found')) + log.warning('Comand %s does not exist: %s', node, jid) + raise nbxmpp.NodeProcessed + + newcmd = self._commands[node] + if not newcmd.isVisibleFor(self.is_same_jid(jid)): + log.warning('Command not visible for jid: %s', jid) + raise nbxmpp.NodeProcessed + + # generate new sessionid + sessionid = self._con.connection.getAnID() + + # create new instance and run it + obj = newcmd(conn=self, jid=jid, sessionid=sessionid) + rc = obj.execute(stanza) + if rc: + self._sessions[(jid, sessionid, node)] = obj + log.info('Comand %s executed: %s', node, jid) + raise nbxmpp.NodeProcessed + else: + # the command is already running, check for it + magictuple = (jid, sessionid, node) + if magictuple not in self._sessions: + # we don't have this session... ha! + log.warning('Invalid session %s', magictuple) + raise nbxmpp.NodeProcessed + + action = cmd.getAttr('action') + obj = self._sessions[magictuple] + + try: + if action == 'cancel': + rc = obj.cancel(stanza) + elif action == 'prev': + rc = obj.prev(stanza) + elif action == 'next': + rc = obj.next(stanza) + elif action == 'execute' or action is None: + rc = obj.execute(stanza) + elif action == 'complete': + rc = obj.complete(stanza) + else: + # action is wrong. stop the session, send error + raise AttributeError + except AttributeError: + # the command probably doesn't handle invoked action... + # stop the session, return error + del self._sessions[magictuple] + log.warning('Wrong action %s %s', node, jid) + raise nbxmpp.NodeProcessed + + # delete the session if rc is False + if not rc: + del self._sessions[magictuple] + + raise nbxmpp.NodeProcessed + + def request_command_list(self, jid): + """ + Request the command list. + """ + log.info('Request Command List: %s', jid) + query = nbxmpp.Iq(typ='get', to=jid, queryNS=nbxmpp.NS_DISCO_ITEMS) + query.setQuerynode(nbxmpp.NS_COMMANDS) + + self._con.connection.SendAndCallForResponse( + query, self._command_list_received) + + def _command_list_received(self, stanza): + if not nbxmpp.isResultNode(stanza): + log.info('Error: %s', stanza.getError()) + + app.nec.push_incoming_event( + AdHocCommandError(None, conn=self._con, + error=stanza.getError())) + return + + items = stanza.getQueryPayload() + commandlist = [] + if items: + commandlist = [(t.getAttr('node'), t.getAttr('name')) for t in items] + + log.info('Received: %s', commandlist) + app.nec.push_incoming_event( + AdHocCommandListReceived( + None, conn=self._con, commandlist=commandlist)) + + def send_command(self, jid, node, session_id, + form, action='execute'): + """ + Send the command with data form. Wait for reply + """ + log.info('Send Command: %s %s %s %s', jid, node, session_id, action) + stanza = nbxmpp.Iq(typ='set', to=jid) + cmdnode = stanza.addChild('command', + namespace=nbxmpp.NS_COMMANDS, + attrs={'node': node, + 'action': action}) + + if session_id: + cmdnode.setAttr('sessionid', session_id) + + if form: + cmdnode.addChild(node=form.get_purged()) + + self._con.connection.SendAndCallForResponse( + stanza, self._action_response_received) + + def _action_response_received(self, stanza): + if not nbxmpp.isResultNode(stanza): + log.info('Error: %s', stanza.getError()) + + app.nec.push_incoming_event( + AdHocCommandError(None, conn=self._con, + error=stanza.getError())) + return + log.info('Received action response') + command = stanza.getTag('command') + app.nec.push_incoming_event( + AdHocCommandActionResponse( + None, conn=self._con, command=command)) + + def send_cancel(self, jid, node, session_id): + """ + Send the command with action='cancel' + """ + log.info('Cancel: %s %s %s', jid, node, session_id) + stanza = nbxmpp.Iq(typ='set', to=jid) + stanza.addChild('command', namespace=nbxmpp.NS_COMMANDS, + attrs={ + 'node': node, + 'sessionid': session_id, + 'action': 'cancel' + }) + + self._con.connection.SendAndCallForResponse( + stanza, self._cancel_result_received) + + def _cancel_result_received(self, stanza): + if not nbxmpp.isResultNode(stanza): + log.warning('Error: %s', stanza.getError()) + else: + log.info('Cancel successful') + + +class AdHocCommandError(NetworkIncomingEvent): + name = 'adhoc-command-error' + base_network_events = [] + + +class AdHocCommandListReceived(NetworkIncomingEvent): + name = 'adhoc-command-list' + base_network_events = [] + + +class AdHocCommandActionResponse(NetworkIncomingEvent): + name = 'adhoc-command-action-response' + base_network_events = [] + + +def get_instance(*args, **kwargs): + return AdHocCommands(*args, **kwargs), 'AdHocCommands' diff --git a/gajim/common/zeroconf/connection_handlers_zeroconf.py b/gajim/common/zeroconf/connection_handlers_zeroconf.py index 9806b1c9e..6f4202a1a 100644 --- a/gajim/common/zeroconf/connection_handlers_zeroconf.py +++ b/gajim/common/zeroconf/connection_handlers_zeroconf.py @@ -26,7 +26,6 @@ import nbxmpp from gajim.common import app -from gajim.common.commands import ConnectionCommands from gajim.common.protocol.bytestream import ConnectionSocks5BytestreamZeroconf from gajim.common.connection_handlers_events import ZeroconfMessageReceivedEvent @@ -49,13 +48,12 @@ class ConnectionVcard: class ConnectionHandlersZeroconf(ConnectionVcard, -ConnectionSocks5BytestreamZeroconf, ConnectionCommands, +ConnectionSocks5BytestreamZeroconf, connection_handlers.ConnectionHandlersBase, connection_handlers.ConnectionJingle): def __init__(self): ConnectionVcard.__init__(self) ConnectionSocks5BytestreamZeroconf.__init__(self) - ConnectionCommands.__init__(self) connection_handlers.ConnectionJingle.__init__(self) connection_handlers.ConnectionHandlersBase.__init__(self) @@ -82,13 +80,13 @@ connection_handlers.ConnectionJingle): if not self.connection or self.connected < 2: return - if self.commandItemsQuery(con, iq_obj): + if self.get_module('AdHocCommands').command_items_query(iq_obj): raise nbxmpp.NodeProcessed node = iq_obj.getTagAttr('query', 'node') if node is None: result = iq_obj.buildReply('result') self.connection.send(result) raise nbxmpp.NodeProcessed - if node==nbxmpp.NS_COMMANDS: - self.commandListQuery(con, iq_obj) + if node == nbxmpp.NS_COMMANDS: + self.get_module('AdHocCommands').command_list_query(iq_obj) raise nbxmpp.NodeProcessed diff --git a/gajim/common/zeroconf/connection_zeroconf.py b/gajim/common/zeroconf/connection_zeroconf.py index fed962bf4..615055178 100644 --- a/gajim/common/zeroconf/connection_zeroconf.py +++ b/gajim/common/zeroconf/connection_zeroconf.py @@ -47,6 +47,7 @@ from gi.repository import GLib from gajim.common.connection import CommonConnection from gajim.common import app from gajim.common import ged +from gajim.common import modules from gajim.common.zeroconf import client_zeroconf from gajim.common.zeroconf import zeroconf from gajim.common.zeroconf.connection_handlers_zeroconf import * @@ -69,6 +70,9 @@ class ConnectionZeroconf(CommonConnection, ConnectionHandlersZeroconf): CommonConnection.__init__(self, name) self.is_zeroconf = True + # Register all modules + modules.register(self) + app.ged.register_event_handler('message-outgoing', ged.OUT_CORE, self._nec_message_outgoing) app.ged.register_event_handler('stanza-message-outgoing', ged.OUT_CORE,