# Copyright 2009 Google Inc. # # 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. import random import time import logging import datetime from django.utils import simplejson from google.appengine.ext import webapp from google.appengine.ext.webapp.util import run_wsgi_app from google.appengine.ext import db from google.appengine.api import quota import settings import quartermile import models STATUS_OK = 100 STATUS_BATCH = 101 STATUS_INVALID_REQUEST = 200 STATUS_INVALID_SIGNATURE = 201 STATUS_NOT_IMPLEMENTED = 300 STATUS_SERVER_ERROR = 666 class ApiError(Exception): def __init__(self, status, message): self.status = status self.message = message def __str__(self): return "[%s] %s" % (self.status, self.message) class JsonRpcHandler(webapp.RequestHandler): """ An implementation of RequestHandler which handles RPC input, delegates to defines methods on the subclass, and renders output to JSON. """ def _encode(self, obj): """ Method used by simplejson to encode model types to JSON. """ if hasattr(obj, "json") and callable(getattr(obj, "json")): return obj.json() elif isinstance(obj, datetime.datetime): return obj.strftime("%a, %d %b %Y %H:%M:%S %Z%z") elif isinstance(obj, datetime.date): return obj.strftime("%d %b %Y") raise TypeError(repr(obj) + " is not JSON serializable") def get_payload(self, status=STATUS_OK, message='', error=False, data={}): """ Returns a standard response payload. """ return { 'status' : status, 'message' : message, 'error' : error, 'data' : data, } def get_rpc_methods(self): """ Should return a dict of string method names mapped to methods. """ return NotImplementedError("get_rpc_methods must be implemented!") def render(self, data): """ Serializes the passed data from this request to JSON and outputs. """ if self.request.get("pretty", False): encoder = simplejson.JSONEncoder(indent=4, sort_keys=True) else: encoder = simplejson.JSONEncoder() encoder.default = self._encode # Monkey patching is fun :) json = encoder.encode(data) self.response.headers.add_header('Content-Type', 'text/plain;charset=UTF-8') self.response.out.write(json) def post(self): """ Handles parsing the RPC request. """ responses = {} had_error = False rpcmethods = self.get_rpc_methods() requests = simplejson.loads(self.request.body) request_items = requests.items() request_items.sort(key=lambda x: x[0]) keys = [] methods = [] for (key, request) in request_items: keys.append(key) try: if not request.has_key('method'): raise ApiError(STATUS_INVALID_REQUEST, "No method supplied") if not rpcmethods.has_key(request['method']): raise ApiError(STATUS_INVALID_REQUEST, "Invalid method") methods.append(request['method']) if request.has_key('data'): data = request['data'] else: data = {} responses[key] = rpcmethods[request['method']](data) except ApiError, ex: had_error = True responses[key] = self.get_payload(status=ex.status, message=ex.message, error=True) payload = self.get_payload(status=STATUS_BATCH, data=responses, error=had_error) payload['batch_order'] = keys self.render(payload) logging.info("requests [%s] cost %s CPU megacycles" % (",".join(methods), quota.get_request_cpu_usage())) def handle_exception(self, exception, debug): if isinstance(exception, db.NeedIndexError): self.render(self.get_payload(status=STATUS_SERVER_ERROR, message='Sigh, waiting on AppEngine. Check back soon!', error=True)) return guid = '%s-%s' % (random.random(), time.time()) message = "Unknown server error! [%s]" % guid logging.critical(message) logging.exception(exception) self.render(self.get_payload(status=STATUS_SERVER_ERROR, message=message, error=True)) def validate_signature(method): def method_wrapper(self, *args, **kwargs): if self.request.get("secure", "1") == "0" and settings.DEBUG: logging.warning("Bypassing signature check!") validated = True else: # TODO: Actual validation code here validated = False if validated: return method(self, *args, **kwargs) else: logging.critical("Invalid signature!") raise ApiError(STATUS_INVALID_SIGNATURE, "Invalid OAuth signature.") return method_wrapper def get_viewer(request): viewer_id = request.get("opensocial_viewer_id", False) consumer_id = request.get("oauth_consumer_key", False) viewer = None if viewer_id and consumer_id: viewer = quartermile.user_get_or_insert(consumer_id, viewer_id) return viewer def require_viewer(method): @validate_signature def method_wrapper(self, *args, **kwargs): self.request.viewer = get_viewer(self.request) if not self.request.viewer: raise ApiError(STATUS_INVALID_REQUEST, "Viewer not found.") return method(self, *args, **kwargs) return method_wrapper class ApiHandler(JsonRpcHandler): def get_rpc_methods(self): return { 'test' : self.test, 'fail' : self.fail, 'create_team' : self.create_team, 'join_team' : self.join_team, 'get_viewer' : self.get_viewer, 'get_teams_by_id' : self.get_teams_by_id, 'get_interesting_teams' : self.get_interesting_teams, 'create_activity' : self.create_activity, 'delete_activity' : self.delete_activity, 'get_activities_by_account' : self.get_activities_by_account, 'get_activities_by_team' : self.get_activities_by_team, 'get_data_by_account' : self.get_data_by_account, 'get_data_by_team' : self.get_data_by_team, 'update_name' : self.update_name, } def fail(self, request): raise ApiError(STATUS_SERVER_ERROR, "Failed on purpose.") def test(self, request): return self.get_payload(data=request, message="Test method.") @require_viewer def create_team(self, request): if not request.has_key('team_name'): raise ApiError(STATUS_INVALID_REQUEST, "No team name supplied.") try: team = quartermile.team_create(self.request.viewer, request['team_name']) return team except quartermile.ConstraintError, ex: raise ApiError(STATUS_INVALID_REQUEST, ex.message) @require_viewer def join_team(self, request): if not request.has_key('team_id'): raise ApiError(STATUS_INVALID_REQUEST, "No team ID supplied.") team = quartermile.model_get_by_id(models.Team, request['team_id']) if len(team) == 0: raise ApiError(STATUS_INVALID_REQUEST, "Invalid team ID specified.") quartermile.team_join(self.request.viewer, team[0]) @require_viewer def get_viewer(self, request): return self.request.viewer.account def get_teams_by_id(self, request): if not request.has_key('team_ids'): raise ApiError(STATUS_INVALID_REQUEST, "No team IDs supplied.") teams = quartermile.model_get_by_id(models.Team, request['team_ids']) return teams def get_interesting_teams(self, request): teams = quartermile.team_get_interesting() if request.has_key('team_ids'): return quartermile.team_get_interesting(request['team_ids']) else: return quartermile.team_get_interesting() @require_viewer def create_activity(self, request): if not request.has_key('text'): raise ApiError(STATUS_INVALID_REQUEST, "No activity text specified.") try: return quartermile.activity_create(self.request.viewer, request['text']) except quartermile.ConstraintError, ex: raise ApiError(STATUS_INVALID_REQUEST, ex.message) @require_viewer def delete_activity(self, request): if not request.has_key('activity_id'): raise ApiError(STATUS_INVALID_REQUEST, "No activity ID specified.") try: return quartermile.activity_delete(self.request.viewer, request['activity_id']) except quartermile.ConstraintError, ex: raise ApiError(STATUS_INVALID_REQUEST, ex.message) def parse_calendar_date(self, datestring): parts = datestring.split(':') if len(parts) != 6: raise ApiError(STATUS_INVALID_REQUEST, "Time formatted wrong, should be in format " + "':::::'.") dateargs = { 'year' : int(parts[0]), 'month' : int(parts[1]), 'day' : int(parts[2]), 'hour' : int(parts[3]), 'minute' : int(parts[4]), 'second' : int(parts[5]), } date = datetime.datetime(**dateargs).replace(tzinfo = models.UTC()) return date def get_activity_kwargs(self, request): kwargs = {} if request.has_key('start'): kwargs['start'] = self.parse_calendar_date(request['start']) if request.has_key('end'): kwargs['end'] = self.parse_calendar_date(request['end']) if request.has_key('page'): kwargs['page'] = int(request['page']) if request.has_key('activity'): kwargs['activity'] = request['activity'] return kwargs def get_account_id(self, request): if request.has_key('account_id'): account_id = request['account_id'] else: viewer = get_viewer(self.request) if not viewer: raise ApiError(STATUS_INVALID_REQUEST, "No account ID found.") account_id = viewer.account.account_id return account_id def get_team_id(self, request): if request.has_key('team_id'): team_id = request['team_id'] else: viewer = get_viewer(self.request) if not viewer: raise ApiError(STATUS_INVALID_REQUEST, "No team ID found.") if not viewer.account.team: raise ApiError(STATUS_INVALID_REQUEST, "You do not belong to a team.") team_id = viewer.account.team.team_id return team_id def get_activities_by_account(self, request): account_id = self.get_account_id(request) kwargs = self.get_activity_kwargs(request) try: return quartermile.activity_get_by_account(account_id, **kwargs) except quartermile.ConstraintError, ex: raise ApiError(STATUS_INVALID_REQUEST, ex.message) def get_activities_by_team(self, request): team_id = self.get_team_id(request) kwargs = self.get_activity_kwargs(request) try: return quartermile.activity_get_by_team(team_id, **kwargs) except quartermile.ConstraintError, ex: raise ApiError(STATUS_INVALID_REQUEST, ex.message) def get_data_by_account(self, request): account_id = self.get_account_id(request) kwargs = self.get_activity_kwargs(request) try: return quartermile.data_get_by_account(account_id, **kwargs) except quartermile.ConstraintError, ex: raise ApiError(STATUS_INVALID_REQUEST, ex.message) def get_data_by_team(self, request): team_id = self.get_team_id(request) kwargs = self.get_activity_kwargs(request) try: return quartermile.data_get_by_team(team_id, **kwargs) except quartermile.ConstraintError, ex: raise ApiError(STATUS_INVALID_REQUEST, ex.message) @require_viewer def update_name(self, request): if not request.has_key('user_name'): raise ApiError(STATUS_INVALID_REQUEST, "No user name specified.") user_name = request['user_name'] try: return quartermile.account_update_name(self.request.viewer, user_name) except quartermile.ConstraintError, ex: raise ApiError(STATUS_INVALID_REQUEST, ex.message) if __name__ == '__main__': handlers = [ ('.*', ApiHandler), ] run_wsgi_app(webapp.WSGIApplication(handlers, debug=settings.DEBUG))