510 lines
18 KiB
Python
510 lines
18 KiB
Python
# -*- 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
|
|
from gajim.common.connection_handlers_events import MessageOutgoingEvent
|
|
|
|
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 ForwardMessagesCommand(AdHocCommand):
|
|
# http://www.xmpp.org/extensions/xep-0146.html#forward
|
|
commandnode = 'forward-messages'
|
|
commandname = _('Forward unread messages')
|
|
|
|
@staticmethod
|
|
def isVisibleFor(samejid):
|
|
"""
|
|
Forward messages is visible only if the entity has the same bare jid
|
|
"""
|
|
return samejid
|
|
|
|
def execute(self, request):
|
|
account = self.connection.name
|
|
# Forward messages
|
|
events = app.events.get_events(account, types=['chat', 'normal',
|
|
'printed_chat'])
|
|
j, resource = app.get_room_and_nick_from_fjid(self.jid)
|
|
for jid in events:
|
|
for event in events[jid]:
|
|
ev_typ = event.type_
|
|
if ev_typ == 'printed_chat':
|
|
ev_typ = 'chat'
|
|
app.nec.push_outgoing_event(MessageOutgoingEvent(None,
|
|
account=account, jid=j, message=event.message, type_=ev_typ,
|
|
subject=event.subject, resource=resource, forward_from=jid,
|
|
delayed=event.time_))
|
|
|
|
# Inform other client of completion
|
|
response, cmd = self.buildResponse(request, status = 'completed')
|
|
cmd.addChild('note', {}, _('All unread messages have been forwarded.'))
|
|
|
|
self.connection.connection.send(response)
|
|
|
|
return False # finish the session
|
|
|
|
class FwdMsgThenDisconnectCommand(AdHocCommand):
|
|
commandnode = 'fwd-msd-disconnect'
|
|
commandname = _('Forward unread message then disconnect')
|
|
|
|
@staticmethod
|
|
def isVisibleFor(samejid):
|
|
"""
|
|
Forward unread messages then disconnect is visible only if the entity has the same bare jid
|
|
"""
|
|
return samejid
|
|
|
|
def execute(self, request):
|
|
account = self.connection.name
|
|
# Forward messages
|
|
events = app.events.get_events(account, types=['chat', 'normal'])
|
|
j, resource = app.get_room_and_nick_from_fjid(self.jid)
|
|
for jid in events:
|
|
for event in events[jid]:
|
|
ev_typ = event.type_
|
|
if ev_typ == 'printed_chat':
|
|
ev_typ = 'chat'
|
|
app.nec.push_outgoing_event(MessageOutgoingEvent(None,
|
|
account=account, jid=j, message=event.message, type_=ev_typ,
|
|
subject=event.subject, resource=resource, forward_from=jid,
|
|
delayed=event.time_, now=True))
|
|
|
|
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 = True)
|
|
|
|
# send new status
|
|
app.interface.roster.send_status(self.connection.name, 'offline', '')
|
|
# finish the session
|
|
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, ForwardMessagesCommand,
|
|
LeaveGroupchatsCommand, FwdMsgThenDisconnectCommand):
|
|
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
|