handle 204 return code for subscription request
[mygpo-feedservice.git] / feedservice / pubsubhubbub.py
blobc5bab344320274e50fa7ec17500867b067e11fe0
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
4 # PubSubHubbub subscriber for mygpo-feedservice
8 import urllib, urllib2, urlparse, logging
9 from datetime import timedelta
11 from google.appengine.ext import webapp, db
13 import urlstore
16 # increased expiry time for subscribed feeds
17 INCREASED_EXPIRY = timedelta(days=7)
20 class SubscriptionError(Exception):
21 pass
24 class SubscribedFeed(db.Model):
25 url = db.StringProperty()
26 verify_token = db.StringProperty()
27 mode = db.StringProperty()
28 verified = db.BooleanProperty()
32 class Subscriber(webapp.RequestHandler):
33 """ request handler for pubsubhubbub subscriptions """
36 def get(self):
37 """ Callback used by the Hub to verify the subscription request """
39 # received arguments: hub.mode, hub.topic, hub.challenge,
40 # hub.lease_seconds, hub.verify_token
41 mode = self.request.get('hub.mode')
42 feed_url = self.request.get('hub.topic')
43 challenge = self.request.get('hub.challenge')
44 lease_seconds = self.request.get('hub.lease_seconds')
45 verify_token = self.request.get('hub.verify_token')
47 logging.debug(('received subscription-parameters: mode: %(mode)s, ' +
48 'topic: %(topic)s, challenge: %(challenge)s, lease_seconds: ' +
49 '%(lease_seconds)s, verify_token: %(verify_token)s') % \
50 dict(mode=mode, topic=feed_url, challenge=challenge,
51 lease_seconds=lease_seconds, verify_token=verify_token))
53 subscription = Subscriber.get_subscription(feed_url)
55 if subscription is None:
56 logging.warn('subscription does not exist')
57 self.response.set_status(404)
58 return
60 if subscription.mode != mode:
61 logging.warn('invalid mode, %s expected' %
62 subscription.mode)
63 self.response.set_status(404)
64 return
66 if subscription.verify_token != verify_token:
67 logging.warn('invalid verify_token, %s expected' %
68 subscribe.verify_token)
69 self.response.set_status(404)
70 return
72 subscription.verified = True
73 subscription.put()
75 logging.info('subscription confirmed')
76 self.response.set_status(200)
77 self.response.out.write(challenge)
81 def post(self):
82 """ Callback to notify about a feed update """
84 feed_url = self.request.get('url')
86 logging.info('received notification for %s' % feed_url)
88 subscription = Subscriber.get_subscription(feed_url)
90 if subscription is None:
91 logging.warn('no subscription for this URL')
92 self.response.set_status(400)
93 return
95 if subscription.mode != 'subscribe':
96 logging.warn('invalid subscription mode: %s' % subscription.mode)
97 self.response.set_status(400)
98 return
100 if not subscription.verified:
101 logging.warn('the subscription has not yet been verified')
102 self.response.set_status(400)
103 return
105 # The changed parts in the POST data are ignored -- we simply fetch the
106 # whole feed.
107 # It is stored in memcache with all the normal (unsubscribed) feeds
108 # but with increased expiry time.
109 urlstore.fetch_url(feed_url, add_expires=INCREASED_EXPIRY)
111 self.response.set_status(200)
114 @staticmethod
115 def get_subscription(feedurl):
116 q = SubscribedFeed.all()
117 q.filter('url =', feedurl)
118 return q.get()
121 @staticmethod
122 def subscribe(feedurl, huburl):
123 """ Subscribe to the feed at a Hub """
125 logging.info('subscribing for %(feed)s at %(hub)s' %
126 dict(feed=feedurl, hub=huburl))
128 verify_token = Subscriber.generate_verify_token()
130 mode = 'subscribe'
131 verify = 'sync'
133 data = {
134 "hub.callback": Subscriber.get_callback_url(feedurl),
135 "hub.mode": mode,
136 "hub.topic": feedurl,
137 "hub.verify": verify,
138 "hub.verify_token": verify_token,
141 subscription = Subscriber.get_subscription(feedurl)
142 if subscription is not None:
144 if subscription.mode == mode:
145 if subscription.verified:
146 logging.info('subscription already exists')
147 return
149 else:
150 logging.info('subscription exists but has wrong mode: ' +
151 'old: %(oldmode)s, new: %(newmode)s. Overwriting.' %
152 dict(oldmode=subscription.mode, newmode=mode))
154 else:
155 subscription = SubscribedFeed()
157 subscription.url = feedurl
158 subscription.verify_token = verify_token
159 subscription.mode = mode
160 subscription.verified = False
161 subscription.put()
163 data = urllib.urlencode(data)
164 logging.debug('sending request: %s' % repr(data))
166 try:
167 resp = urllib2.urlopen(huburl, data)
168 except urllib2.HTTPError, e:
169 if e.code != 204: # we actually expect a 204 return code
170 msg = 'Could not send subscription to Hub: HTTP Error %d' % e.code
171 logging.warn(msg)
172 raise SubscriptionError(msg)
173 except Exception, e:
174 msg = 'Could not send subscription to Hub: %s' % repr(e)
175 logging.warn(msg)
176 raise SubscriptionError(msg)
179 status = resp.code
180 if status != 204:
181 logging.error('received incorrect status %d' % status)
182 raise SubscriptionError('Subscription has not been accepted by the Hub')
186 @staticmethod
187 def get_callback_url(feedurl):
188 import settings
189 url = urlparse.urljoin(settings.BASE_URL, 'subscribe')
191 param = urllib.urlencode([('url', feedurl)])
192 return '%s?%s' % (url, param)
195 @staticmethod
196 def generate_verify_token(length=32):
197 import random, string
198 return "".join(random.sample(string.letters+string.digits, length))