├── .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 |
12 |
13 |
14 |
19 |
20 | {% if feeds %}
21 |
22 |
23 | {% for feed in feeds %}
24 | {{ feed.title }}
25 |
28 | {% endfor %}
29 |
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 |
--------------------------------------------------------------------------------