blob: 776f82581db83d50f266c03f3fa36d8b045c5ca2 [file] [log] [blame]
#!/usr/bin/python
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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)