client-json: produce slightly more valid JSON
[uccdoor.git] / xmpp / commands.py
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')]

UCC git Repository :: git.ucc.asn.au