root/xmppony/commands.py @ 1:0603290d9eb7

Revision 1:0603290d9eb7, 15.9 kB (checked in by elghinn, 18 months ago)

* welcome new headers (licence + copyright)

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