| # Copyright 2014 Google Inc. All Rights Reserved. | |
| # | |
| # 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. | |
| """Channel notifications support. | |
| Classes and functions to support channel subscriptions and notifications | |
| on those channels. | |
| Notes: | |
| - This code is based on experimental APIs and is subject to change. | |
| - Notification does not do deduplication of notification ids, that's up to | |
| the receiver. | |
| - Storing the Channel between calls is up to the caller. | |
| Example setting up a channel: | |
| # Create a new channel that gets notifications via webhook. | |
| channel = new_webhook_channel("https://example.com/my_web_hook") | |
| # Store the channel, keyed by 'channel.id'. Store it before calling the | |
| # watch method because notifications may start arriving before the watch | |
| # method returns. | |
| ... | |
| resp = service.objects().watchAll( | |
| bucket="some_bucket_id", body=channel.body()).execute() | |
| channel.update(resp) | |
| # Store the channel, keyed by 'channel.id'. Store it after being updated | |
| # since the resource_id value will now be correct, and that's needed to | |
| # stop a subscription. | |
| ... | |
| An example Webhook implementation using webapp2. Note that webapp2 puts | |
| headers in a case insensitive dictionary, as headers aren't guaranteed to | |
| always be upper case. | |
| id = self.request.headers[X_GOOG_CHANNEL_ID] | |
| # Retrieve the channel by id. | |
| channel = ... | |
| # Parse notification from the headers, including validating the id. | |
| n = notification_from_headers(channel, self.request.headers) | |
| # Do app specific stuff with the notification here. | |
| if n.resource_state == 'sync': | |
| # Code to handle sync state. | |
| elif n.resource_state == 'exists': | |
| # Code to handle the exists state. | |
| elif n.resource_state == 'not_exists': | |
| # Code to handle the not exists state. | |
| Example of unsubscribing. | |
| service.channels().stop(channel.body()).execute() | |
| """ | |
| from __future__ import absolute_import | |
| import datetime | |
| import uuid | |
| from googleapiclient import errors | |
| from googleapiclient import _helpers as util | |
| import six | |
| # The unix time epoch starts at midnight 1970. | |
| EPOCH = datetime.datetime.utcfromtimestamp(0) | |
| # Map the names of the parameters in the JSON channel description to | |
| # the parameter names we use in the Channel class. | |
| CHANNEL_PARAMS = { | |
| "address": "address", | |
| "id": "id", | |
| "expiration": "expiration", | |
| "params": "params", | |
| "resourceId": "resource_id", | |
| "resourceUri": "resource_uri", | |
| "type": "type", | |
| "token": "token", | |
| } | |
| X_GOOG_CHANNEL_ID = "X-GOOG-CHANNEL-ID" | |
| X_GOOG_MESSAGE_NUMBER = "X-GOOG-MESSAGE-NUMBER" | |
| X_GOOG_RESOURCE_STATE = "X-GOOG-RESOURCE-STATE" | |
| X_GOOG_RESOURCE_URI = "X-GOOG-RESOURCE-URI" | |
| X_GOOG_RESOURCE_ID = "X-GOOG-RESOURCE-ID" | |
| def _upper_header_keys(headers): | |
| new_headers = {} | |
| for k, v in six.iteritems(headers): | |
| new_headers[k.upper()] = v | |
| return new_headers | |
| class Notification(object): | |
| """A Notification from a Channel. | |
| Notifications are not usually constructed directly, but are returned | |
| from functions like notification_from_headers(). | |
| Attributes: | |
| message_number: int, The unique id number of this notification. | |
| state: str, The state of the resource being monitored. | |
| uri: str, The address of the resource being monitored. | |
| resource_id: str, The unique identifier of the version of the resource at | |
| this event. | |
| """ | |
| @util.positional(5) | |
| def __init__(self, message_number, state, resource_uri, resource_id): | |
| """Notification constructor. | |
| Args: | |
| message_number: int, The unique id number of this notification. | |
| state: str, The state of the resource being monitored. Can be one | |
| of "exists", "not_exists", or "sync". | |
| resource_uri: str, The address of the resource being monitored. | |
| resource_id: str, The identifier of the watched resource. | |
| """ | |
| self.message_number = message_number | |
| self.state = state | |
| self.resource_uri = resource_uri | |
| self.resource_id = resource_id | |
| class Channel(object): | |
| """A Channel for notifications. | |
| Usually not constructed directly, instead it is returned from helper | |
| functions like new_webhook_channel(). | |
| Attributes: | |
| type: str, The type of delivery mechanism used by this channel. For | |
| example, 'web_hook'. | |
| id: str, A UUID for the channel. | |
| token: str, An arbitrary string associated with the channel that | |
| is delivered to the target address with each event delivered | |
| over this channel. | |
| address: str, The address of the receiving entity where events are | |
| delivered. Specific to the channel type. | |
| expiration: int, The time, in milliseconds from the epoch, when this | |
| channel will expire. | |
| params: dict, A dictionary of string to string, with additional parameters | |
| controlling delivery channel behavior. | |
| resource_id: str, An opaque id that identifies the resource that is | |
| being watched. Stable across different API versions. | |
| resource_uri: str, The canonicalized ID of the watched resource. | |
| """ | |
| @util.positional(5) | |
| def __init__( | |
| self, | |
| type, | |
| id, | |
| token, | |
| address, | |
| expiration=None, | |
| params=None, | |
| resource_id="", | |
| resource_uri="", | |
| ): | |
| """Create a new Channel. | |
| In user code, this Channel constructor will not typically be called | |
| manually since there are functions for creating channels for each specific | |
| type with a more customized set of arguments to pass. | |
| Args: | |
| type: str, The type of delivery mechanism used by this channel. For | |
| example, 'web_hook'. | |
| id: str, A UUID for the channel. | |
| token: str, An arbitrary string associated with the channel that | |
| is delivered to the target address with each event delivered | |
| over this channel. | |
| address: str, The address of the receiving entity where events are | |
| delivered. Specific to the channel type. | |
| expiration: int, The time, in milliseconds from the epoch, when this | |
| channel will expire. | |
| params: dict, A dictionary of string to string, with additional parameters | |
| controlling delivery channel behavior. | |
| resource_id: str, An opaque id that identifies the resource that is | |
| being watched. Stable across different API versions. | |
| resource_uri: str, The canonicalized ID of the watched resource. | |
| """ | |
| self.type = type | |
| self.id = id | |
| self.token = token | |
| self.address = address | |
| self.expiration = expiration | |
| self.params = params | |
| self.resource_id = resource_id | |
| self.resource_uri = resource_uri | |
| def body(self): | |
| """Build a body from the Channel. | |
| Constructs a dictionary that's appropriate for passing into watch() | |
| methods as the value of body argument. | |
| Returns: | |
| A dictionary representation of the channel. | |
| """ | |
| result = { | |
| "id": self.id, | |
| "token": self.token, | |
| "type": self.type, | |
| "address": self.address, | |
| } | |
| if self.params: | |
| result["params"] = self.params | |
| if self.resource_id: | |
| result["resourceId"] = self.resource_id | |
| if self.resource_uri: | |
| result["resourceUri"] = self.resource_uri | |
| if self.expiration: | |
| result["expiration"] = self.expiration | |
| return result | |
| def update(self, resp): | |
| """Update a channel with information from the response of watch(). | |
| When a request is sent to watch() a resource, the response returned | |
| from the watch() request is a dictionary with updated channel information, | |
| such as the resource_id, which is needed when stopping a subscription. | |
| Args: | |
| resp: dict, The response from a watch() method. | |
| """ | |
| for json_name, param_name in six.iteritems(CHANNEL_PARAMS): | |
| value = resp.get(json_name) | |
| if value is not None: | |
| setattr(self, param_name, value) | |
| def notification_from_headers(channel, headers): | |
| """Parse a notification from the webhook request headers, validate | |
| the notification, and return a Notification object. | |
| Args: | |
| channel: Channel, The channel that the notification is associated with. | |
| headers: dict, A dictionary like object that contains the request headers | |
| from the webhook HTTP request. | |
| Returns: | |
| A Notification object. | |
| Raises: | |
| errors.InvalidNotificationError if the notification is invalid. | |
| ValueError if the X-GOOG-MESSAGE-NUMBER can't be converted to an int. | |
| """ | |
| headers = _upper_header_keys(headers) | |
| channel_id = headers[X_GOOG_CHANNEL_ID] | |
| if channel.id != channel_id: | |
| raise errors.InvalidNotificationError( | |
| "Channel id mismatch: %s != %s" % (channel.id, channel_id) | |
| ) | |
| else: | |
| message_number = int(headers[X_GOOG_MESSAGE_NUMBER]) | |
| state = headers[X_GOOG_RESOURCE_STATE] | |
| resource_uri = headers[X_GOOG_RESOURCE_URI] | |
| resource_id = headers[X_GOOG_RESOURCE_ID] | |
| return Notification(message_number, state, resource_uri, resource_id) | |
| @util.positional(2) | |
| def new_webhook_channel(url, token=None, expiration=None, params=None): | |
| """Create a new webhook Channel. | |
| Args: | |
| url: str, URL to post notifications to. | |
| token: str, An arbitrary string associated with the channel that | |
| is delivered to the target address with each notification delivered | |
| over this channel. | |
| expiration: datetime.datetime, A time in the future when the channel | |
| should expire. Can also be None if the subscription should use the | |
| default expiration. Note that different services may have different | |
| limits on how long a subscription lasts. Check the response from the | |
| watch() method to see the value the service has set for an expiration | |
| time. | |
| params: dict, Extra parameters to pass on channel creation. Currently | |
| not used for webhook channels. | |
| """ | |
| expiration_ms = 0 | |
| if expiration: | |
| delta = expiration - EPOCH | |
| expiration_ms = ( | |
| delta.microseconds / 1000 + (delta.seconds + delta.days * 24 * 3600) * 1000 | |
| ) | |
| if expiration_ms < 0: | |
| expiration_ms = 0 | |
| return Channel( | |
| "web_hook", | |
| str(uuid.uuid4()), | |
| token, | |
| url, | |
| expiration=expiration_ms, | |
| params=params, | |
| ) |