Refactor AdHocCommands into own module

This commit is contained in:
Philipp Hörist 2018-07-08 18:37:53 +02:00
parent 78d16c44f3
commit a2d7283e6e
7 changed files with 642 additions and 539 deletions

View File

@ -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)

View File

@ -1,433 +0,0 @@
# -*- coding:utf-8 -*-
## src/common/commands.py
##
## Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
## Copyright (C) 2006-2007 Tomasz Melcer <liori AT exroot.org>
## Copyright (C) 2007 Jean-Marie Traissard <jim AT lapin.org>
## Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
## Stephan Erb <steve-e AT h3c.de>
##
## 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 <http://www.gnu.org/licenses/>.
##
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

View File

@ -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',

View File

@ -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):

View File

@ -0,0 +1,573 @@
# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2006-2007 Tomasz Melcer <liori AT exroot.org>
# Copyright (C) 2007 Jean-Marie Traissard <jim AT lapin.org>
# Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
# Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
#
# 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 <http://www.gnu.org/licenses/>.
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'

View File

@ -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

View File

@ -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,