#!/usr/bin/python # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """A module to run wave robots on app engine.""" import logging import sys import events from google.appengine.api import urlfetch from google.appengine.ext import webapp from google.appengine.ext.webapp.util import run_wsgi_app class CapabilitiesHandler(webapp.RequestHandler): """Handler to forward a request ot a handler of a robot.""" def __init__(self, method, contenttype): """Initializes this handler with a specific robot.""" self._method = method self._contenttype = contenttype def get(self): """Handles HTTP GET request.""" self.response.headers['Content-Type'] = self._contenttype self.response.out.write(self._method()) class ProfileHandler(webapp.RequestHandler): """Handler to forward a request ot a handler of a robot.""" def __init__(self, method, contenttype): """Initializes this handler with a specific robot.""" self._method = method self._contenttype = contenttype def get(self): """Handles HTTP GET request.""" self.response.headers['Content-Type'] = self._contenttype # Respond with proxied profile if name specified if self.request.get('name'): self.response.out.write(self._method(self.request.get('name'))) else: self.response.out.write(self._method()) class RobotEventHandler(webapp.RequestHandler): """Handler for the dispatching of events to various handlers to a robot. This handler only responds to post events with a JSON post body. Its primary task is to separate out the context data from the events in the post body and dispatch all events in order. Once all events have been dispatched it serializes the context data and its associated operations as a response. """ def __init__(self, robot): """Initializes self with a specific robot.""" self._robot = robot def get(self): """Handles the get event for debugging. This is useful for debugging but since event bundles tend to be rather big it often won't fit for more complex requests. """ ops = self.request.get('events') if ops: self.request.body = events self.post() def post(self): """Handles HTTP POST requests.""" json_body = self.request.body if not json_body: # TODO(davidbyttow): Log error? return # Redirect stdout to stderr while executing handlers. This way, any stray # "print" statements in bot code go to the error logs instead of breaking # the JSON response sent to the HTTP channel. saved_stdout, sys.stdout = sys.stdout, sys.stderr json_body = unicode(json_body, 'utf8') logging.info('Incoming: %s', json_body) json_response = self._robot.process_events(json_body) logging.info('Outgoing: %s', json_response) sys.stdout = saved_stdout # Build the response. self.response.headers['Content-Type'] = 'application/json; charset=utf-8' self.response.out.write(json_response.encode('utf-8')) def operation_error_handler(event, wavelet): """Default operation error handler, logging what went wrong.""" if isinstance(event, events.OperationError): logging.error('Previously operation failed: id=%s, message: %s', event.operation_id, event.error_message) def appengine_post(url, data, headers): result = urlfetch.fetch( method='POST', url=url, payload=data, headers=headers, deadline=10) return result.status_code, result.content class RobotVerifyTokenHandler(webapp.RequestHandler): """Handler for the token_verify request.""" def __init__(self, robot): """Initializes self with a specific robot.""" self._robot = robot def get(self): """Handles the get event for debugging. Ops usually too long.""" token, st = self._robot.get_verification_token_info() logging.info('token=' + token) if token is None: self.error(404) self.response.out.write('No token set') return if st is not None: if self.request.get('st') != st: self.response.out.write('Invalid st value passed') return self.response.out.write(token) def create_robot_webapp(robot, debug=False, extra_handlers=None): """Returns an instance of webapp.WSGIApplication with robot handlers.""" if not extra_handlers: extra_handlers = [] return webapp.WSGIApplication([('.*/_wave/capabilities.xml', lambda: CapabilitiesHandler( robot.capabilities_xml, 'application/xml')), ('.*/_wave/robot/profile', lambda: ProfileHandler( robot.profile_json, 'application/json')), ('.*/_wave/robot/jsonrpc', lambda: RobotEventHandler(robot)), ('.*/_wave/verify_token', lambda: RobotVerifyTokenHandler(robot)), ] + extra_handlers, debug=debug) def run(robot, debug=False, log_errors=True, extra_handlers=None): """Sets up the webapp handlers for this robot and starts listening. A robot is typically setup in the following steps: 1. Instantiate and define robot. 2. Register various handlers that it is interested in. 3. Call Run, which will setup the handlers for the app. For example: robot = Robot('Terminator', image_url='http://www.sky.net/models/t800.png', profile_url='http://www.sky.net/models/t800.html') robot.register_handler(WAVELET_PARTICIPANTS_CHANGED, KillParticipant) run(robot) Args: robot: the robot to run. This robot is modified to use app engines urlfetch for posting http. debug: Optional variable that defaults to False and is passed through to the webapp application to determine if it should show debug info. log_errors: Optional flag that defaults to True and determines whether a default handlers to catch errors should be setup that uses the app engine logging to log errors. extra_handlers: Optional list of tuples that are passed to the webapp to install more handlers. For example, passing [('/about', AboutHandler),] would install an extra about handler for the robot. """ # App Engine expects to construct a class with no arguments, so we # pass a lambda that constructs the appropriate handler with # arguments from the enclosing scope. if log_errors: robot.register_handler(events.OperationError, operation_error_handler) robot.set_http_post(appengine_post) app = create_robot_webapp(robot, debug, extra_handlers) run_wsgi_app(app)