gajim-plural/gajim/common/modules/adhoc_commands.py

576 lines
20 KiB
Python

# 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 nbxmpp
from nbxmpp.structs import StanzaHandler
from gajim.common import app
from gajim.common import helpers
from gajim.common.i18n import _
from gajim.common.nec import NetworkIncomingEvent
from gajim.common.modules import dataforms
from gajim.common.modules.base import BaseModule
class AdHocCommand:
commandnode = 'command'
commandname = 'The Command'
commandfeatures = (nbxmpp.NS_DATA,)
@staticmethod
def is_visible_for(_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 build_response(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 bad_request(self, stanza):
self.connection.connection.send(
nbxmpp.Error(stanza, nbxmpp.NS_STANZAS + ' bad-request'))
def cancel(self, request):
response = self.build_response(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._callback = self.first_step
@staticmethod
def is_visible_for(samejid):
"""
Change status is visible only if the entity has the same bare jid
"""
return samejid
def execute(self, request):
return self._callback(request)
def first_step(self, request):
# first query...
response, cmd = self.build_response(request,
defaultaction='execute',
actions=['execute'])
cmd.addChild(
node=dataforms.SimpleDataForm(
title=_('Change status'),
instructions=_('Set the presence type and description'),
fields=[
dataforms.create_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.create_field(
'text-multi',
var='presence-desc',
label=_('Presence description:'))
]
)
)
self.connection.connection.send(response)
# for next invocation
self._callback = 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.bad_request(request)
return False
try:
presencetype = form['presence-type'].value
if presencetype not in ('chat', 'online', 'away',
'xa', 'dnd', 'offline'):
self.bad_request(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.bad_request(request)
return False
try:
presencedesc = form['presence-desc'].value
except Exception: # same exceptions as in last comment
presencedesc = ''
response, cmd = self.build_response(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._callback = self.first_step
@staticmethod
def is_visible_for(samejid):
"""
Leave groupchats is visible only if the entity has the same bare jid
"""
return samejid
def execute(self, request):
return self._callback(request)
def first_step(self, request):
# first query...
response, cmd = self.build_response(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 options:
response, cmd = self.build_response(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.create_field(
'list-multi',
var='groupchats',
label=_('Groupchats'),
options=options,
required=True)
]
)
)
self.connection.connection.send(response)
# for next invocation
self._callback = 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.bad_request(request)
return False
try:
gc = form['groupchats'].values
except Exception: # KeyError if there's no groupchats in form
self.bad_request(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:
minimized_controls = app.interface.minimized_controls
gc_control = 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.bad_request(request)
return False
response, cmd = self.build_response(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(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='iq',
callback=self._execute_command_received,
typ='set',
ns=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)
query = iq.getTag('query')
# buildReply don't copy the node attribute. Re-add it
query.setAttr('node', nbxmpp.NS_COMMANDS)
for node, cmd in self._commands.items():
if cmd.is_visible_for(self.is_same_jid(jid)):
query.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:
self._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.is_visible_for(self.is_same_jid(jid)):
iq = stanza.buildReply('result')
query = iq.getTag('query')
query.addChild('identity',
attrs={'type': 'command-node',
'category': 'automation',
'name': cmd.commandname})
query.addChild('feature', attrs={'var': nbxmpp.NS_COMMANDS})
for feature in cmd.commandfeatures:
query.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.is_visible_for(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, _properties):
jid = helpers.get_full_jid_from_iq(stanza)
cmd = stanza.getTag('command')
if cmd is None:
self._log.error('Malformed stanza (no command node) %s', stanza)
raise nbxmpp.NodeProcessed
node = cmd.getAttr('node')
if node is None:
self._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'))
self._log.warning('Comand %s does not exist: %s', node, jid)
raise nbxmpp.NodeProcessed
newcmd = self._commands[node]
if not newcmd.is_visible_for(self.is_same_jid(jid)):
self._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
self._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!
self._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]
self._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.
"""
self._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):
self._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
]
self._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
"""
self._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):
self._log.info('Error: %s', stanza.getError())
app.nec.push_incoming_event(
AdHocCommandError(None, conn=self._con,
error=stanza.getError()))
return
self._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'
"""
self._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):
self._log.warning('Error: %s', stanza.getError())
else:
self._log.info('Cancel successful')
class AdHocCommandError(NetworkIncomingEvent):
name = 'adhoc-command-error'
class AdHocCommandListReceived(NetworkIncomingEvent):
name = 'adhoc-command-list'
class AdHocCommandActionResponse(NetworkIncomingEvent):
name = 'adhoc-command-action-response'
def get_instance(*args, **kwargs):
return AdHocCommands(*args, **kwargs), 'AdHocCommands'