Package xmpp :: Module commands

[hide private]
[frames] | no frames]

# Source Code for Module xmpp.commands

      1  ## $Id: commands.py,v 1.17 2007/08/28 09:54:15 normanr Exp $
      2
      3  ## Ad-Hoc Command manager
      4  ## Mike Albon (c) 5th January 2005
      5
      6  ##   This program is free software; you can redistribute it and/or modify
      7  ##   it under the terms of the GNU General Public License as published by
      8  ##   the Free Software Foundation; either version 2, or (at your option)
      9  ##   any later version.
     10  ##
     11  ##   This program is distributed in the hope that it will be useful,
     12  ##   but WITHOUT ANY WARRANTY; without even the implied warranty of
     13  ##   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     14  ##   GNU General Public License for more details.
     15
     16
     17  """This module is a ad-hoc command processor for xmpppy. It uses the plug-in mechanism like most of the core library. It depends on a DISCO browser manager.
     18
     19  There are 3 classes here, a command processor Commands like the Browser, and a command template plugin Command, and an example command.
     20
     21  To use this module:
     22
     23      Instansiate the module with the parent transport and disco browser manager as parameters.
     24      'Plug in' commands using the command template.
     25      The command feature must be added to existing disco replies where neccessary.
     26
     27  What it supplies:
     28
     29      Automatic command registration with the disco browser manager.
     30      Automatic listing of commands in the public command list.
     31      A means of handling requests, by redirection though the command manager.
     32  """
     33
     34  from protocol import *
     35  from client import PlugIn
     36
     37 -class Commands(PlugIn):
     38      """Commands is an ancestor of PlugIn and can be attached to any session.
     39
     40      The commands class provides a lookup and browse mechnism. It follows the same priciple of the Browser class, for Service Discovery to provide the list of commands, it adds the 'list' disco type to your existing disco handler function.
     41
     42      How it works:
     43          The commands are added into the existing Browser on the correct nodes. When the command list is built the supplied discovery handler function needs to have a 'list' option in type. This then gets enumerated, all results returned as None are ignored.
     44          The command executed is then called using it's Execute method. All session management is handled by the command itself.
     45      """
     46 -    def __init__(self, browser):
     47          """Initialises class and sets up local variables"""
     48          PlugIn.__init__(self)
     49          DBG_LINE='commands'
     50          self._exported_methods=[]
     51          self._handlers={'':{}}
     52          self._browser = browser
     53
     54 -    def plugin(self, owner):
     55          """Makes handlers within the session"""
     56          # Plug into the session and the disco manager
     57          # We only need get and set, results are not needed by a service provider, only a service user.
     58          owner.RegisterHandler('iq',self._CommandHandler,typ='set',ns=NS_COMMANDS)
     59          owner.RegisterHandler('iq',self._CommandHandler,typ='get',ns=NS_COMMANDS)
     60          self._browser.setDiscoHandler(self._DiscoHandler,node=NS_COMMANDS,jid='')
     61
     62 -    def plugout(self):
     63          """Removes handlers from the session"""
     64          # unPlug from the session and the disco manager
     65          self._owner.UnregisterHandler('iq',self._CommandHandler,ns=NS_COMMANDS)
     66          for jid in self._handlers:
     67              self._browser.delDiscoHandler(self._DiscoHandler,node=NS_COMMANDS)
     68
     69 -    def _CommandHandler(self,conn,request):
     70          """The internal method to process the routing of command execution requests"""
     71          # This is the command handler itself.
     72          # We must:
     73          #   Pass on command execution to command handler
     74          #   (Do we need to keep session details here, or can that be done in the command?)
     75          jid = str(request.getTo())
     76          try:
     77              node = request.getTagAttr('command','node')
     78          except:
     79              conn.send(Error(request,ERR_BAD_REQUEST))
     80              raise NodeProcessed
     81          if self._handlers.has_key(jid):
     82              if self._handlers[jid].has_key(node):
     83                  self._handlers[jid][node]['execute'](conn,request)
     84              else:
     85                  conn.send(Error(request,ERR_ITEM_NOT_FOUND))
     86                  raise NodeProcessed
     87          elif self._handlers[''].has_key(node):
     88                  self._handlers[''][node]['execute'](conn,request)
     89          else:
     90              conn.send(Error(request,ERR_ITEM_NOT_FOUND))
     91              raise NodeProcessed
     92
     93 -    def _DiscoHandler(self,conn,request,typ):
     94          """The internal method to process service discovery requests"""
     95          # This is the disco manager handler.
     96          if typ == 'items':
     97              # We must:
     98              #    Generate a list of commands and return the list
     99              #    * This handler does not handle individual commands disco requests.
    100              # Pseudo:
    101              #   Enumerate the 'item' disco of each command for the specified jid
    102              #   Build responce and send
    103              #   To make this code easy to write we add an 'list' disco type, it returns a tuple or 'none' if not advertised
    104              list = []
    105              items = []
    106              jid = str(request.getTo())
    107              # Get specific jid based results
    108              if self._handlers.has_key(jid):
    109                  for each in self._handlers[jid].keys():
    110                      items.append((jid,each))
    111              else:
    112                  # Get generic results
    113                  for each in self._handlers[''].keys():
    114                      items.append(('',each))
    115              if items != []:
    116                  for each in items:
    117                      i = self._handlers[each[0]][each[1]]['disco'](conn,request,'list')
    118                      if i != None:
    119                          list.append(Node(tag='item',attrs={'jid':i[0],'node':i[1],'name':i[2]}))
    120                  iq = request.buildReply('result')
    121                  if request.getQuerynode(): iq.setQuerynode(request.getQuerynode())
    122                  iq.setQueryPayload(list)
    123                  conn.send(iq)
    124              else:
    125                  conn.send(Error(request,ERR_ITEM_NOT_FOUND))
    126              raise NodeProcessed
    127          elif typ == 'info':
    128              return {'ids':[{'category':'automation','type':'command-list'}],'features':[]}
    129
    130 -    def addCommand(self,name,cmddisco,cmdexecute,jid=''):
    131          """The method to call if adding a new command to the session, the requred parameters of cmddisco and cmdexecute are the methods to enable that command to be executed"""
    132          # This command takes a command object and the name of the command for registration
    133          # We must:
    134          #   Add item into disco
    135          #   Add item into command list
    136          if not self._handlers.has_key(jid):
    137              self._handlers[jid]={}
    138              self._browser.setDiscoHandler(self._DiscoHandler,node=NS_COMMANDS,jid=jid)
    139          if self._handlers[jid].has_key(name):
    140              raise NameError,'Command Exists'
    141          else:
    142              self._handlers[jid][name]={'disco':cmddisco,'execute':cmdexecute}
    143          # Need to add disco stuff here
    144          self._browser.setDiscoHandler(cmddisco,node=name,jid=jid)
    145
    146 -    def delCommand(self,name,jid=''):
    147          """Removed command from the session"""
    148          # This command takes a command object and the name used for registration
    149          # We must:
    150          #   Remove item from disco
    151          #   Remove item from command list
    152          if not self._handlers.has_key(jid):
    153              raise NameError,'Jid not found'
    154          if not self._handlers[jid].has_key(name):
    155              raise NameError, 'Command not found'
    156          else:
    157              #Do disco removal here
    158              command = self.getCommand(name,jid)['disco']
    159              del self._handlers[jid][name]
    160              self._browser.delDiscoHandler(command,node=name,jid=jid)
    161
    162 -    def getCommand(self,name,jid=''):
    163          """Returns the command tuple"""
    164          # This gets the command object with name
    165          # We must:
    166          #   Return item that matches this name
    167          if not self._handlers.has_key(jid):
    168              raise NameError,'Jid not found'
    169          elif not self._handlers[jid].has_key(name):
    170              raise NameError,'Command not found'
    171          else:
    172              return self._handlers[jid][name]
    173
    174 -class Command_Handler_Prototype(PlugIn):
    175      """This is a prototype command handler, as each command uses a disco method
    176         and execute method you can implement it any way you like, however this is
    177         my first attempt at making a generic handler that you can hang process
    178         stages on too. There is an example command below.
    179
    180      The parameters are as follows:
    181      name : the name of the command within the jabber environment
    182      description : the natural language description
    183      discofeatures : the features supported by the command
    184      initial : the initial command in the from of {'execute':commandname}
    185
    186      All stages set the 'actions' dictionary for each session to represent the possible options available.
    187      """
    188      name = 'examplecommand'
    189      count = 0
    190      description = 'an example command'
    191      discofeatures = [NS_COMMANDS,NS_DATA]
    192      # This is the command template
    193 -    def __init__(self,jid=''):
    194          """Set up the class"""
    195          PlugIn.__init__(self)
    196          DBG_LINE='command'
    197          self.sessioncount = 0
    198          self.sessions = {}
    199          # Disco information for command list pre-formatted as a tuple
    200          self.discoinfo = {'ids':[{'category':'automation','type':'command-node','name':self.description}],'features': self.discofeatures}
    201          self._jid = jid
    202
    203 -    def plugin(self,owner):
    204          """Plug command into the commands class"""
    205          # The owner in this instance is the Command Processor
    206          self._commands = owner
    207          self._owner = owner._owner
    208          self._commands.addCommand(self.name,self._DiscoHandler,self.Execute,jid=self._jid)
    209
    210 -    def plugout(self):
    211          """Remove command from the commands class"""
    212          self._commands.delCommand(self.name,self._jid)
    213
    214 -    def getSessionID(self):
    215          """Returns an id for the command session"""
    216          self.count = self.count+1
    217          return 'cmd-%s-%d'%(self.name,self.count)
    218
    219 -    def Execute(self,conn,request):
    220          """The method that handles all the commands, and routes them to the correct method for that stage."""
    221          # New request or old?
    222          try:
    223              session = request.getTagAttr('command','sessionid')
    224          except:
    225              session = None
    226          try:
    227              action = request.getTagAttr('command','action')
    228          except:
    229              action = None
    230          if action == None: action = 'execute'
    231          # Check session is in session list
    232          if self.sessions.has_key(session):
    233              if self.sessions[session]['jid']==request.getFrom():
    234                  # Check action is vaild
    235                  if self.sessions[session]['actions'].has_key(action):
    236                      # Execute next action
    237                      self.sessions[session]['actions'][action](conn,request)
    238                  else:
    239                      # Stage not presented as an option
    240                      self._owner.send(Error(request,ERR_BAD_REQUEST))
    241                      raise NodeProcessed
    242              else:
    243                  # Jid and session don't match. Go away imposter
    244                  self._owner.send(Error(request,ERR_BAD_REQUEST))
    245                  raise NodeProcessed
    246          elif session != None:
    247              # Not on this sessionid you won't.
    248              self._owner.send(Error(request,ERR_BAD_REQUEST))
    249              raise NodeProcessed
    250          else:
    251              # New session
    252              self.initial[action](conn,request)
    253
    254 -    def _DiscoHandler(self,conn,request,type):
    255          """The handler for discovery events"""
    256          if type == 'list':
    257              return (request.getTo(),self.name,self.description)
    258          elif type == 'items':
    259              return []
    260          elif type == 'info':
    261              return self.discoinfo
    262
    263 -class TestCommand(Command_Handler_Prototype):
    264      """ Example class. You should read source if you wish to understate how it works.
    265          Generally, it presents a "master" that giudes user through to calculate something.
    266      """
    267      name = 'testcommand'
    268      description = 'a noddy example command'
    269 -    def __init__(self,jid=''):
    270          """ Init internal constants. """
    271          Command_Handler_Prototype.__init__(self,jid)
    272          self.initial = {'execute':self.cmdFirstStage}
    273
    274 -    def cmdFirstStage(self,conn,request):
    275          """ Determine """
    276          # This is the only place this should be repeated as all other stages should have SessionIDs
    277          try:
    278              session = request.getTagAttr('command','sessionid')
    279          except:
    280              session = None
    281          if session == None:
    282              session = self.getSessionID()
    283              self.sessions[session]={'jid':request.getFrom(),'actions':{'cancel':self.cmdCancel,'next':self.cmdSecondStage,'execute':self.cmdSecondStage},'data':{'type':None}}
    284          # As this is the first stage we only send a form
    285          reply = request.buildReply('result')
    286          form = DataForm(title='Select type of operation',data=['Use the combobox to select the type of calculation you would like to do, then click Next',DataField(name='calctype',desc='Calculation Type',value=self.sessions[session]['data']['type'],options=[['circlediameter','Calculate the Diameter of a circle'],['circlearea','Calculate the area of a circle']],typ='list-single',required=1)])
    287          replypayload = [Node('actions',attrs={'execute':'next'},payload=[Node('next')]),form]
    288          reply.addChild(name='command',namespace=NS_COMMANDS,attrs={'node':request.getTagAttr('command','node'),'sessionid':session,'status':'executing'},payload=replypayload)
    289          self._owner.send(reply)
    290          raise NodeProcessed
    291
    292 -    def cmdSecondStage(self,conn,request):
    293          form = DataForm(node = request.getTag(name='command').getTag(name='x',namespace=NS_DATA))
    294          self.sessions[request.getTagAttr('command','sessionid')]['data']['type']=form.getField('calctype').getValue()
    295          self.sessions[request.getTagAttr('command','sessionid')]['actions']={'cancel':self.cmdCancel,None:self.cmdThirdStage,'previous':self.cmdFirstStage,'execute':self.cmdThirdStage,'next':self.cmdThirdStage}
    296          # The form generation is split out to another method as it may be called by cmdThirdStage
    297          self.cmdSecondStageReply(conn,request)
    298
    299 -    def cmdSecondStageReply(self,conn,request):
    300          reply = request.buildReply('result')
    301          form = DataForm(title = 'Enter the radius', data=['Enter the radius of the circle (numbers only)',DataField(desc='Radius',name='radius',typ='text-single')])
    302          replypayload = [Node('actions',attrs={'execute':'complete'},payload=[Node('complete'),Node('prev')]),form]
    303          reply.addChild(name='command',namespace=NS_COMMANDS,attrs={'node':request.getTagAttr('command','node'),'sessionid':request.getTagAttr('command','sessionid'),'status':'executing'},payload=replypayload)
    304          self._owner.send(reply)
    305          raise NodeProcessed
    306
    307 -    def cmdThirdStage(self,conn,request):
    308          form = DataForm(node = request.getTag(name='command').getTag(name='x',namespace=NS_DATA))
    309          try:
    310              num = float(form.getField('radius').getValue())
    311          except:
    312              self.cmdSecondStageReply(conn,request)
    313          from math import pi
    314          if self.sessions[request.getTagAttr('command','sessionid')]['data']['type'] == 'circlearea':
    315              result = (num**2)*pi
    316          else:
    317              result = num*2*pi
    318          reply = request.buildReply('result')
    319          form = DataForm(typ='result',data=[DataField(desc='result',name='result',value=result)])
    320          reply.addChild(name='command',namespace=NS_COMMANDS,attrs={'node':request.getTagAttr('command','node'),'sessionid':request.getTagAttr('command','sessionid'),'status':'completed'},payload=[form])
    321          self._owner.send(reply)
    322          raise NodeProcessed
    323
    324 -    def cmdCancel(self,conn,request):
    325          reply = request.buildReply('result')
    326          reply.addChild(name='command',namespace=NS_COMMANDS,attrs={'node':request.getTagAttr('command','node'),'sessionid':request.getTagAttr('command','sessionid'),'status':'cancelled'})
    327          self._owner.send(reply)
    328          del self.sessions[request.getTagAttr('command','sessionid')]
    329