| OLD | NEW |
| (Empty) |
| 1 # Licensed under the Apache License, Version 2.0 (the "License"); | |
| 2 # you may not use this file except in compliance with the License. | |
| 3 # You may obtain a copy of the License at | |
| 4 # | |
| 5 # http://www.apache.org/licenses/LICENSE-2.0 | |
| 6 # | |
| 7 # Unless required by applicable law or agreed to in writing, software | |
| 8 # distributed under the License is distributed on an "AS IS" BASIS, | |
| 9 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 10 # See the License for the specific language governing permissions and | |
| 11 # limitations under the License. | |
| 12 | |
| 13 """Push notifications support. | |
| 14 | |
| 15 This code is based on experimental APIs and is subject to change. | |
| 16 """ | |
| 17 | |
| 18 __author__ = 'afshar@google.com (Ali Afshar)' | |
| 19 | |
| 20 import binascii | |
| 21 import collections | |
| 22 import os | |
| 23 import urllib | |
| 24 | |
| 25 SUBSCRIBE = 'X-GOOG-SUBSCRIBE' | |
| 26 SUBSCRIPTION_ID = 'X-GOOG-SUBSCRIPTION-ID' | |
| 27 TOPIC_ID = 'X-GOOG-TOPIC-ID' | |
| 28 TOPIC_URI = 'X-GOOG-TOPIC-URI' | |
| 29 CLIENT_TOKEN = 'X-GOOG-CLIENT-TOKEN' | |
| 30 EVENT_TYPE = 'X-GOOG-EVENT-TYPE' | |
| 31 UNSUBSCRIBE = 'X-GOOG-UNSUBSCRIBE' | |
| 32 | |
| 33 | |
| 34 class InvalidSubscriptionRequestError(ValueError): | |
| 35 """The request cannot be subscribed.""" | |
| 36 | |
| 37 | |
| 38 def new_token(): | |
| 39 """Gets a random token for use as a client_token in push notifications. | |
| 40 | |
| 41 Returns: | |
| 42 str, a new random token. | |
| 43 """ | |
| 44 return binascii.hexlify(os.urandom(32)) | |
| 45 | |
| 46 | |
| 47 class Channel(object): | |
| 48 """Base class for channel types.""" | |
| 49 | |
| 50 def __init__(self, channel_type, channel_args): | |
| 51 """Create a new Channel. | |
| 52 | |
| 53 You probably won't need to create this channel manually, since there are | |
| 54 subclassed Channel for each specific type with a more customized set of | |
| 55 arguments to pass. However, you may wish to just create it manually here. | |
| 56 | |
| 57 Args: | |
| 58 channel_type: str, the type of channel. | |
| 59 channel_args: dict, arguments to pass to the channel. | |
| 60 """ | |
| 61 self.channel_type = channel_type | |
| 62 self.channel_args = channel_args | |
| 63 | |
| 64 def as_header_value(self): | |
| 65 """Create the appropriate header for this channel. | |
| 66 | |
| 67 Returns: | |
| 68 str encoded channel description suitable for use as a header. | |
| 69 """ | |
| 70 return '%s?%s' % (self.channel_type, urllib.urlencode(self.channel_args)) | |
| 71 | |
| 72 def write_header(self, headers): | |
| 73 """Write the appropriate subscribe header to a headers dict. | |
| 74 | |
| 75 Args: | |
| 76 headers: dict, headers to add subscribe header to. | |
| 77 """ | |
| 78 headers[SUBSCRIBE] = self.as_header_value() | |
| 79 | |
| 80 | |
| 81 class WebhookChannel(Channel): | |
| 82 """Channel for registering web hook notifications.""" | |
| 83 | |
| 84 def __init__(self, url, app_engine=False): | |
| 85 """Create a new WebhookChannel | |
| 86 | |
| 87 Args: | |
| 88 url: str, URL to post notifications to. | |
| 89 app_engine: bool, default=False, whether the destination for the | |
| 90 notifications is an App Engine application. | |
| 91 """ | |
| 92 super(WebhookChannel, self).__init__( | |
| 93 channel_type='web_hook', | |
| 94 channel_args={ | |
| 95 'url': url, | |
| 96 'app_engine': app_engine and 'true' or 'false', | |
| 97 } | |
| 98 ) | |
| 99 | |
| 100 | |
| 101 class Headers(collections.defaultdict): | |
| 102 """Headers for managing subscriptions.""" | |
| 103 | |
| 104 | |
| 105 ALL_HEADERS = set([SUBSCRIBE, SUBSCRIPTION_ID, TOPIC_ID, TOPIC_URI, | |
| 106 CLIENT_TOKEN, EVENT_TYPE, UNSUBSCRIBE]) | |
| 107 | |
| 108 def __init__(self): | |
| 109 """Create a new subscription configuration instance.""" | |
| 110 collections.defaultdict.__init__(self, str) | |
| 111 | |
| 112 def __setitem__(self, key, value): | |
| 113 """Set a header value, ensuring the key is an allowed value. | |
| 114 | |
| 115 Args: | |
| 116 key: str, the header key. | |
| 117 value: str, the header value. | |
| 118 Raises: | |
| 119 ValueError if key is not one of the accepted headers. | |
| 120 """ | |
| 121 normal_key = self._normalize_key(key) | |
| 122 if normal_key not in self.ALL_HEADERS: | |
| 123 raise ValueError('Header name must be one of %s.' % self.ALL_HEADERS) | |
| 124 else: | |
| 125 return collections.defaultdict.__setitem__(self, normal_key, value) | |
| 126 | |
| 127 def __getitem__(self, key): | |
| 128 """Get a header value, normalizing the key case. | |
| 129 | |
| 130 Args: | |
| 131 key: str, the header key. | |
| 132 Returns: | |
| 133 String header value. | |
| 134 Raises: | |
| 135 KeyError if the key is not one of the accepted headers. | |
| 136 """ | |
| 137 normal_key = self._normalize_key(key) | |
| 138 if normal_key not in self.ALL_HEADERS: | |
| 139 raise ValueError('Header name must be one of %s.' % self.ALL_HEADERS) | |
| 140 else: | |
| 141 return collections.defaultdict.__getitem__(self, normal_key) | |
| 142 | |
| 143 def _normalize_key(self, key): | |
| 144 """Normalize a header name for use as a key.""" | |
| 145 return key.upper() | |
| 146 | |
| 147 def items(self): | |
| 148 """Generator for each header.""" | |
| 149 for header in self.ALL_HEADERS: | |
| 150 value = self[header] | |
| 151 if value: | |
| 152 yield header, value | |
| 153 | |
| 154 def write(self, headers): | |
| 155 """Applies the subscription headers. | |
| 156 | |
| 157 Args: | |
| 158 headers: dict of headers to insert values into. | |
| 159 """ | |
| 160 for header, value in self.items(): | |
| 161 headers[header.lower()] = value | |
| 162 | |
| 163 def read(self, headers): | |
| 164 """Read from headers. | |
| 165 | |
| 166 Args: | |
| 167 headers: dict of headers to read from. | |
| 168 """ | |
| 169 for header in self.ALL_HEADERS: | |
| 170 if header.lower() in headers: | |
| 171 self[header] = headers[header.lower()] | |
| 172 | |
| 173 | |
| 174 class Subscription(object): | |
| 175 """Information about a subscription.""" | |
| 176 | |
| 177 def __init__(self): | |
| 178 """Create a new Subscription.""" | |
| 179 self.headers = Headers() | |
| 180 | |
| 181 @classmethod | |
| 182 def for_request(cls, request, channel, client_token=None): | |
| 183 """Creates a subscription and attaches it to a request. | |
| 184 | |
| 185 Args: | |
| 186 request: An http.HttpRequest to modify for making a subscription. | |
| 187 channel: A apiclient.push.Channel describing the subscription to | |
| 188 create. | |
| 189 client_token: (optional) client token to verify the notification. | |
| 190 | |
| 191 Returns: | |
| 192 New subscription object. | |
| 193 """ | |
| 194 subscription = cls.for_channel(channel=channel, client_token=client_token) | |
| 195 subscription.headers.write(request.headers) | |
| 196 if request.method != 'GET': | |
| 197 raise InvalidSubscriptionRequestError( | |
| 198 'Can only subscribe to requests which are GET.') | |
| 199 request.method = 'POST' | |
| 200 | |
| 201 def _on_response(response, subscription=subscription): | |
| 202 """Called with the response headers. Reads the subscription headers.""" | |
| 203 subscription.headers.read(response) | |
| 204 | |
| 205 request.add_response_callback(_on_response) | |
| 206 return subscription | |
| 207 | |
| 208 @classmethod | |
| 209 def for_channel(cls, channel, client_token=None): | |
| 210 """Alternate constructor to create a subscription from a channel. | |
| 211 | |
| 212 Args: | |
| 213 channel: A apiclient.push.Channel describing the subscription to | |
| 214 create. | |
| 215 client_token: (optional) client token to verify the notification. | |
| 216 | |
| 217 Returns: | |
| 218 New subscription object. | |
| 219 """ | |
| 220 subscription = cls() | |
| 221 channel.write_header(subscription.headers) | |
| 222 if client_token is None: | |
| 223 client_token = new_token() | |
| 224 subscription.headers[SUBSCRIPTION_ID] = new_token() | |
| 225 subscription.headers[CLIENT_TOKEN] = client_token | |
| 226 return subscription | |
| 227 | |
| 228 def verify(self, headers): | |
| 229 """Verifies that a webhook notification has the correct client_token. | |
| 230 | |
| 231 Args: | |
| 232 headers: dict of request headers for a push notification. | |
| 233 | |
| 234 Returns: | |
| 235 Boolean value indicating whether the notification is verified. | |
| 236 """ | |
| 237 new_subscription = Subscription() | |
| 238 new_subscription.headers.read(headers) | |
| 239 return new_subscription.client_token == self.client_token | |
| 240 | |
| 241 @property | |
| 242 def subscribe(self): | |
| 243 """Subscribe header value.""" | |
| 244 return self.headers[SUBSCRIBE] | |
| 245 | |
| 246 @property | |
| 247 def subscription_id(self): | |
| 248 """Subscription ID header value.""" | |
| 249 return self.headers[SUBSCRIPTION_ID] | |
| 250 | |
| 251 @property | |
| 252 def topic_id(self): | |
| 253 """Topic ID header value.""" | |
| 254 return self.headers[TOPIC_ID] | |
| 255 | |
| 256 @property | |
| 257 def topic_uri(self): | |
| 258 """Topic URI header value.""" | |
| 259 return self.headers[TOPIC_URI] | |
| 260 | |
| 261 @property | |
| 262 def client_token(self): | |
| 263 """Client Token header value.""" | |
| 264 return self.headers[CLIENT_TOKEN] | |
| 265 | |
| 266 @property | |
| 267 def event_type(self): | |
| 268 """Event Type header value.""" | |
| 269 return self.headers[EVENT_TYPE] | |
| 270 | |
| 271 @property | |
| 272 def unsubscribe(self): | |
| 273 """Unsuscribe header value.""" | |
| 274 return self.headers[UNSUBSCRIBE] | |
| OLD | NEW |