├── .gitignore ├── static ├── robots.txt ├── .DS_Store ├── favicon.ico └── style.css ├── app.yaml ├── index.yaml ├── main.html └── main.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | key.py 3 | -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / -------------------------------------------------------------------------------- /static/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progrium/feednotifier/master/static/.DS_Store -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progrium/feednotifier/master/static/favicon.ico -------------------------------------------------------------------------------- /static/style.css: -------------------------------------------------------------------------------- 1 | body { font-family: Georgia; } 2 | h1 { font-size: 50px; letter-spacing: 6} 3 | #wrapper { width: 525px; margin-left: auto; margin-right: auto; } 4 | #login-button { font-size: 20px; margin-left: 150px; } -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | application: feednotifier 2 | version: 1 3 | runtime: python 4 | api_version: 1 5 | 6 | handlers: 7 | - url: / 8 | script: main.py 9 | - url: /favicon.ico 10 | static_files: static/favicon.ico 11 | upload: static/favicon.ico 12 | - url: /robots.txt 13 | static_files: static/robots.txt 14 | upload: static/robots.txt 15 | - url: /static 16 | static_dir: static 17 | - url: .* 18 | script: main.py 19 | -------------------------------------------------------------------------------- /index.yaml: -------------------------------------------------------------------------------- 1 | indexes: 2 | 3 | # AUTOGENERATED 4 | 5 | # This index.yaml is automatically updated whenever the dev_appserver 6 | # detects that a new type of query is run. If you want to manage the 7 | # index.yaml file manually, remove the above marker line (the line 8 | # saying "# AUTOGENERATED"). If you want to manage some indexes 9 | # manually, move them above the marker line. The index.yaml file is 10 | # automatically uploaded to the admin console when you next deploy 11 | # your application using appcfg.py. 12 | -------------------------------------------------------------------------------- /main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Feed Notifier :: Get real-time notifications of feed updates 4 | 5 | 6 | 7 |
8 |

Feed Notifier

9 | {% if user %} 10 | 11 |
{{ user.nickname }} | Logout
12 | 13 |

14 |

15 | Feed URL:
16 | 17 | 18 |
19 |

20 | {% if feeds %} 21 |

22 |

30 |

31 | {% endif %} 32 | 33 | {% else %} 34 |

Feed Notifier uses your notify.io account to send you real-time notifications of feed updates. All you have to do is login with Google and enter a PubSubHubbub-enabled feed URL. Then awesome happens.

35 |

Login with Google

36 | 37 | 38 | {% endif %} 39 |
40 | 44 | 49 | 50 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import wsgiref.handlers 4 | 5 | from google.appengine.api.labs import taskqueue 6 | from google.appengine.ext import webapp 7 | from google.appengine.api import users 8 | from google.appengine.ext.webapp import template 9 | from google.appengine.ext import db 10 | from google.appengine.api import urlfetch 11 | from google.appengine.api import mail 12 | from xml.dom import minidom 13 | import time, urllib, os, hashlib 14 | import key 15 | 16 | def notify(user, text, title, link=None): 17 | params = {'text':text,'title':title, 'icon': 'http://feednotifier.appspot.com/favicon.ico'} 18 | if link: 19 | params['link'] = link 20 | urlfetch.fetch('http://api.notify.io/v1/notify/%s?api_key=%s' % (hashlib.md5(user.email()).hexdigest(), key.api_key), method='POST', payload=urllib.urlencode(params)) 21 | 22 | class Feed(db.Model): 23 | user = db.UserProperty(auto_current_user_add=True) 24 | url = db.StringProperty(required=True) 25 | title = db.StringProperty() 26 | hub_url = db.StringProperty(required=True) 27 | created = db.DateTimeProperty(auto_now_add=True) 28 | updated = db.DateTimeProperty(auto_now=True) 29 | 30 | class MainHandler(webapp.RequestHandler): 31 | def get(self): 32 | user = users.get_current_user() 33 | if user: 34 | logout_url = users.create_logout_url("/") 35 | feeds = Feed.all().filter('user =', user) 36 | else: 37 | login_url = users.create_login_url('/') 38 | self.response.out.write(template.render('main.html', locals())) 39 | 40 | def post(self): 41 | user = users.get_current_user() 42 | if self.request.get('id'): 43 | feed = Feed.get_by_id(int(self.request.get('id'))) 44 | if feed.user == user: 45 | feed.delete() 46 | self.redirect('/') 47 | 48 | feed_url = self.request.get('url') 49 | try: 50 | feed_string = unicode(urlfetch.fetch(feed_url).content.strip(), "utf-8").encode('ascii', 'xmlcharrefreplace') 51 | except urlfetch.InvalidURLError: 52 | self.response.out.write("Not a valid URL") 53 | return 54 | feed_dom = minidom.parseString(feed_string) 55 | for link in feed_dom.getElementsByTagName('link'): 56 | if link.getAttribute('rel') == 'hub': 57 | # PubSubHubbub enabled feed 58 | 59 | hub_url = link.getAttribute('href') 60 | title = [x.firstChild.data for x in feed_dom.getElementsByTagName('feed')[0].childNodes if x.nodeName == 'title'] 61 | title = title[0] if title else None 62 | feed = Feed(url=feed_url, hub_url=hub_url, title=title) 63 | feed.put() 64 | 65 | taskqueue.add(url='/subscribe', params={'id': feed.key().id()}) 66 | 67 | self.redirect('/') 68 | 69 | self.response.out.write("Not a feed or not a PubSubHubbub enabled feed") 70 | 71 | class SubscribeHandler(webapp.RequestHandler): 72 | def post(self): 73 | feed_id = self.request.get('id') 74 | feed = Feed.get_by_id(int(feed_id)) 75 | if feed: 76 | params = { 77 | 'hub.mode': 'subscribe', 78 | 'hub.callback': 'http://www.feednotifier.org/notify/%s' % feed_id, 79 | 'hub.topic': feed.url, 80 | 'hub.verify': 'sync', 81 | 'hub.verify_token': feed_id, 82 | } 83 | urlfetch.fetch(feed.hub_url, method='POST', payload=urllib.urlencode(params)) 84 | 85 | 86 | class NotifyHandler(webapp.RequestHandler): 87 | def get(self): 88 | topic = self.request.get('hub.topic') 89 | feed_id = self.request.get('hub.verify_token') 90 | feed = Feed.get_by_id(int(feed_id)) 91 | if feed.url == topic: 92 | notify(feed.user, "Subscribed to updates", feed.title) 93 | self.response.out.write(self.request.get('hub.challenge')) 94 | else: 95 | self.error(404) 96 | 97 | def post(self): 98 | feed_id = self.request.path.split('/')[-1] 99 | feed = Feed.get_by_id(int(feed_id)) 100 | feed_dom = minidom.parseString(self.request.body.encode('utf-8', 'xmlcharrefreplace')) 101 | for entry in feed_dom.getElementsByTagName('entry'): 102 | entry_title = entry.getElementsByTagName('title')[0].firstChild 103 | entry_title = entry_title.data if entry_title else "???" 104 | notify(feed.user, entry_title, feed.title or feed.url) 105 | 106 | def main(): 107 | application = webapp.WSGIApplication([ 108 | ('/', MainHandler), 109 | ('/subscribe', SubscribeHandler), 110 | ('/notify/.*', NotifyHandler), 111 | ], debug=True) 112 | wsgiref.handlers.CGIHandler().run(application) 113 | 114 | if __name__ == '__main__': 115 | main() 116 | --------------------------------------------------------------------------------