318 lines
11 KiB
Python
318 lines
11 KiB
Python
|
# 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,
|
||
|
)
|