# -*- 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 common import helpers from common import dataforms from common import gajim from 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') @staticmethod def isVisibleFor(samejid): """ Change status is visible only if the entity has the same bare jid """ return samejid def execute(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.execute = self.changestatus return True # keep the session def changestatus(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 gajim.interface.roster.send_status(self.connection.name, presencetype, presencedesc) return False # finish the session def find_current_groupchats(account): import message_control rooms = [] for gc_control in gajim.interface.msg_win_mgr.get_controls( message_control.TYPE_GC) + gajim.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 gajim.gc_connected[acct] and \ gajim.gc_connected[acct][room_jid]: rooms.append((room_jid, nick,)) return rooms class LeaveGroupchatsCommand(AdHocCommand): commandnode = 'leave-groupchats' commandname = _('Leave Groupchats') @staticmethod def isVisibleFor(samejid): """ Change status is visible only if the entity has the same bare jid """ return samejid def execute(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.execute = self.leavegroupchats return True # keep the session def leavegroupchats(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 = gajim.interface.msg_win_mgr.get_gc_control(room_jid, account) if not gc_control: gc_control = gajim.interface.minimized_controls[account]\ [room_jid] gc_control.shutdown() gajim.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): """ Change status is visible only if the entity has the same bare jid """ return samejid def execute(self, request): account = self.connection.name # Forward messages events = gajim.events.get_events(account, types=['chat', 'normal', 'printed_chat']) j, resource = gajim.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' gajim.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): """ Change status is visible only if the entity has the same bare jid """ return samejid def execute(self, request): account = self.connection.name # Forward messages events = gajim.events.get_events(account, types=['chat', 'normal']) j, resource = gajim.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' gajim.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 gajim.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 = {} 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 gajim.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