#!/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. """Defines the generic robot classes. This module provides the Robot class and RobotListener interface, as well as some helper functions for web requests and responses. """ import logging import sys try: __import__('google3') # setup internal test environment except ImportError: pass import simplejson import blip import errors import ops import simplejson import wavelet import waveservice DEFAULT_PROFILE_URL = ( 'http://code.google.com/apis/wave/extensions/robots/python-tutorial.html') class Robot(object): """Robot metadata class. This class holds on to basic robot information like the name and profile. It also maintains the list of event handlers and cron jobs and dispatches events to the appropriate handlers. """ def __init__(self, name, image_url='', profile_url=DEFAULT_PROFILE_URL): """Initializes self with robot information. Args: name: The name of the robot image_url: (optional) url of an image that should be used as the avatar for this robot. profile_url: (optional) url of a webpage with more information about this robot. """ self._handlers = {} self._name = name self._verification_token = None self._st = None self._waveservice = waveservice.WaveService() self._profile_handler = None self._image_url = image_url self._profile_url = profile_url self._capability_hash = 0 self._consumer_key = None self._http_post = None @property def name(self): """Returns the name of the robot.""" return self._name @property def image_url(self): """Returns the URL of the avatar image.""" return self._image_url @property def profile_url(self): """Returns the URL of an info page for the robot.""" return self._profile_url def get_verification_token_info(self): """Returns the verification token and ST parameter.""" return self._verification_token, self._st def get_waveservice(self): """Return the currently installed waveservice if available. Raises: Error: if no service is installed. """ if self._waveservice is None: raise errors.Error('Oauth has not been setup') return self._waveservice def capabilities_hash(self): """Return the capabilities hash as a hex string.""" return hex(self._capability_hash) def register_handler(self, event_class, handler, context=None, filter=None): """Registers a handler on a specific event type. Multiple handlers may be registered on a single event type and are guaranteed to be called in order of registration. The handler takes two arguments, the event object and the corresponding wavelet. Args: event_class: An event to listen for from the classes defined in the events module. handler: A function handler which takes two arguments, the wavelet for the event and the event object. context: The context to provide for this handler. filter: Depending on the event, a filter can be specified that restricts for which values the event handler will be called from the server. Valuable to restrict the amount of traffic send to the robot. """ payload = (handler, event_class, context, filter) self._handlers.setdefault(event_class.type, []).append(payload) if isinstance(context, list): context = ','.join(context) self._capability_hash = (self._capability_hash * 13 + hash(ops.PROTOCOL_VERSION) + hash(event_class.type) + hash(context) + hash(filter)) & 0xfffffff def set_verification_token_info(self, token, st=None): """Set the verification token used in the ownership verification. /wave/robot/register starts this process up and will produce this token. Args: token: the token provided by /wave/robot/register. st: optional parameter to verify the request for the token came from the wave server. """ self._verification_token = token self._st = st def set_http_post(self, http_post): """Set the http_post handler to use when posting.""" self._http_post = http_post if self._waveservice: self._waveservice.set_http_post(http_post) def setup_oauth(self, consumer_key, consumer_secret, server_rpc_base='https://www-opensocial.googleusercontent.com/api/rpc'): """Configure this robot to use the oauth'd json rpc. Args: consumer_key: consumer key received from the verification process. consumer_secret: secret received from the verification process. server_rpc_base: url of the rpc gateway to use. Specify None for default. For wave preview, https://www-opensocial.googleusercontent.com/api/rpc should be used. For wave sandbox, https://www-opensocial-sandbox.googleusercontent.com/api/rpc should be used. """ consumer_key_prefix = '' # NOTE(ljvderijk): Present for backwards capability. if server_rpc_base in [waveservice.WaveService.SANDBOX_RPC_URL, waveservice.WaveService.RPC_URL]: consumer_key_prefix = 'google.com:' self._consumer_key = consumer_key_prefix + consumer_key self._waveservice = waveservice.WaveService( consumer_key=consumer_key, consumer_secret=consumer_secret, server_rpc_base=server_rpc_base, http_post=self._http_post) def register_profile_handler(self, handler): """Sets the profile handler for this robot. The profile handler will be called when a profile is needed. The handler gets passed the name for which a profile is needed or None for the robot itself. A dictionary with keys for name, imageUrl and profileUrl should be returned. """ self._profile_handler = handler def capabilities_xml(self): """Return this robot's capabilities as an XML string.""" lines = [] for capability, payloads in self._handlers.items(): for payload in payloads: handler, event_class, context, filter = payload line = ' \n' '\n' '%s\n' '%s' '%s\n' '\n' '%s' '\n' '\n') % (self.capabilities_hash(), oauth_tag, ops.PROTOCOL_VERSION, '\n'.join(lines)) def profile_json(self, name=None): """Returns a JSON representation of the profile. This method is called both for the basic profile of the robot and to get a proxying for profile, in which case name is set. By default the information supplied at registration is returned. Use register_profile_handler to override this default behavior. """ if self._profile_handler: data = self._profile_handler(name) else: data = {'name': self.name, 'imageUrl': self.image_url, 'profileUrl': self.profile_url} return simplejson.dumps(data) def process_events(self, json): """Process an incoming set of events encoded as json.""" parsed = simplejson.loads(json) pending_ops = ops.OperationQueue() event_wavelet = self.get_waveservice()._wavelet_from_json(parsed, pending_ops) for event_data in parsed['events']: for payload in self._handlers.get(event_data['type'], []): handler, event_class, context, filter = payload event = event_class(event_data, event_wavelet) handler(event, event_wavelet) pending_ops.set_capability_hash(self.capabilities_hash()) return simplejson.dumps(pending_ops.serialize()) def new_wave(self, domain, participants=None, message='', proxy_for_id=None, submit=False): """Create a new wave with the initial participants on it. A new wave is returned with its own operation queue. It the responsibility of the caller to make sure this wave gets submitted to the server, either by calling robot.submit() or by calling .submit_with() on the returned wave. Args: domain: the domain to create the wavelet on. This should in general correspond to the domain of the incoming wavelet. (wavelet.domain). Exceptions are situations where the robot is calling new_wave outside of an event or when the server is handling multiple domains. participants: initial participants on the wave. The robot as the creator of the wave is always added. message: a string that will be passed back to the robot when the WAVELET_CREATOR event is fired. This is a lightweight way to pass around state. submit: if true, use the active gateway to make a round trip to the server. This will return immediately an actual waveid/waveletid and blipId for the root blip. """ return self.get_waveservice().new_wave( domain, participants, message, proxy_for_id, submit) def fetch_wavelet(self, wave_id, wavelet_id=None, proxy_for_id=None, raw_deltas_from_version=-1, return_raw_snapshot=False): """Use the REST interface to fetch a wave and return it. The returned wavelet contains a snapshot of the state of the wavelet at that point. It can be used to modify the wavelet, but the wavelet might change in between, so treat carefully. Also note that the wavelet returned has its own operation queue. It the responsibility of the caller to make sure this wavelet gets submited to the server, either by calling robot.submit() or by calling .submit_with() on the returned wavelet. """ return self.get_waveservice().fetch_wavelet( wave_id, wavelet_id, proxy_for_id, raw_deltas_from_version, return_raw_snapshot) def blind_wavelet(self, json, proxy_for_id=None): """Construct a blind wave from a json string. Call this method if you have a snapshot of a wave that you want to operate on outside of an event. Since the wave might have changed since you last saw it, you should take care to submit operations that are as safe as possible. Args: json: a json object or string containing at least a key wavelet defining the wavelet and a key blips defining the blips in the view. proxy_for_id: the proxying information that will be set on the wavelet's operation queue. Returns: A new wavelet with its own operation queue. It the responsibility of the caller to make sure this wavelet gets submited to the server, either by calling robot.submit() or by calling .submit_with() on the returned wavelet. """ return self.get_waveservice().blind_wavelet(json, proxy_for_id) def submit(self, wavelet_to_submit): """Submit the pending operations associated with wavelet_to_submit. Typically the wavelet will be the result of fetch_wavelet, blind_wavelet or new_wave. """ return self.get_waveservice().submit(wavelet_to_submit)