add realtime update notification

This commit is contained in:
Kyle Mahan 2015-02-22 21:55:45 -08:00
parent f8eee5261a
commit 52a8564b97
6 changed files with 145 additions and 55 deletions

32
websocket_server.js Normal file
View file

@ -0,0 +1,32 @@
var WebSocketServer = require('ws').Server;
var Redis = require('redis');
var port = 8077;
var wss = new WebSocketServer({port: port});
wss.on('connection', function(ws) {
// console.log("New websockets connection");
ws.on('message', function(channel) {
var redis = Redis.createClient(6379, 'localhost');
redis.subscribe(channel);
// console.log('Listening for comments on channel ' + channel);
redis.on('message', function (channel, message) {
console.log('Sent comment to channel ' + channel);
ws.send(message);
});
ws.on('close', function(){
// console.log('Killing listener for channel ' + channel);
redis.unsubscribe();
redis.end();
});
ws.on('error', function(){
// console.log('Killing listener for channel ' + channel);
redis.unsubscribe();
redis.end();
});
});
});
console.log("WebSocket Server Listening on port "+port);

View file

@ -66,7 +66,5 @@ $(function(){
}); });
} }
attachListeners(); attachListeners();
}); });

View file

@ -0,0 +1,23 @@
// topic will be woodwind::user:id or woodwind::feed:id
function webSocketSubscribe(topic) {
if ('WebSocket' in window) {
var ws = new WebSocket(window.location.origin
.replace(/https?:\/\//, 'ws://')
.replace(/(:\d+)?$/, ':8077'));
ws.onopen = function(event) {
// send the topic
console.log('subscribing to topic: ' + topic);
ws.send(topic);
};
ws.onmessage = function(event) {
var data = JSON.parse(event.data);
data.entries.forEach(function(entryHtml) {
$('body main').prepend(entryHtml);
});
};
}
}

View file

@ -1,11 +1,13 @@
from config import Config from config import Config
from contextlib import contextmanager from contextlib import contextmanager
from woodwind.models import Feed, Entry from woodwind.models import Feed, Entry
from redis import StrictRedis
import bs4 import bs4
import celery import celery
import celery.utils.log import celery.utils.log
import datetime import datetime
import feedparser import feedparser
import json
import mf2py import mf2py
import mf2util import mf2util
import re import re
@ -27,6 +29,8 @@ logger = celery.utils.log.get_task_logger(__name__)
engine = sqlalchemy.create_engine(Config.SQLALCHEMY_DATABASE_URI) engine = sqlalchemy.create_engine(Config.SQLALCHEMY_DATABASE_URI)
Session = sqlalchemy.orm.sessionmaker(bind=engine) Session = sqlalchemy.orm.sessionmaker(bind=engine)
redis = StrictRedis()
@contextmanager @contextmanager
def session_scope(): def session_scope():
@ -65,7 +69,7 @@ def update_feed(feed_id):
def process_feed(session, feed): def process_feed(session, feed):
now = datetime.datetime.utcnow() now = datetime.datetime.utcnow()
found_new = False new_entries = []
try: try:
logger.info('fetching feed: %s', feed) logger.info('fetching feed: %s', feed)
response = requests.get(feed.feed) response = requests.get(feed.feed)
@ -102,14 +106,17 @@ def process_feed(session, feed):
for in_reply_to in entry.get_property('in-reply-to', []): for in_reply_to in entry.get_property('in-reply-to', []):
fetch_reply_context.delay(entry.id, in_reply_to) fetch_reply_context.delay(entry.id, in_reply_to)
found_new = True new_entries.append(entry)
else: else:
logger.info('skipping previously seen post %s', old.permalink) logger.info('skipping previously seen post %s', old.permalink)
finally: finally:
feed.last_checked = now feed.last_checked = now
if found_new: if new_entries:
feed.last_updated = now feed.last_updated = now
session.commit()
if new_entries:
notify_feed_updated(session, feed, new_entries)
def check_push_subscription(session, feed, response): def check_push_subscription(session, feed, response):
@ -168,6 +175,28 @@ def check_push_subscription(session, feed, response):
send_request('subscribe', hub, topic) send_request('subscribe', hub, topic)
def notify_feed_updated(session, feed, entries):
"""Render the new entries and publish them to redis
"""
from . import create_app
from flask import render_template
import flask.ext.login as flask_login
flask_app = create_app()
for user in feed.users:
with flask_app.test_request_context():
flask_login.login_user(user, remember=True)
message = json.dumps({
'user': user.id,
'feed': feed.id,
'entries': [
render_template('_entry.jinja2', feed=feed, entry=e)
for e in entries
],
})
redis.publish('woodwind:user:{}'.format(user.id), message)
def is_content_equal(e1, e2): def is_content_equal(e1, e2):
"""The criteria for determining if an entry that we've seen before """The criteria for determining if an entry that we've seen before
has been updated. If any of these fields have changed, we'll scrub the has been updated. If any of these fields have changed, we'll scrub the

View file

@ -0,0 +1,50 @@
{% for context in entry.reply_context %}
<article class="reply-context">
<header>
{% if context.author_photo %}
<img src="{{context.author_photo}}"/>
{% endif %}
{% if context.author_name %}
{{ context.author_name }} -
{% endif %}
{{ context.permalink | domain_for_url }}
</header>
{% if context.title %}
<h1>{{ context.title }}</h1>
{% endif %}
{% if context.content %}
<div>
{{ context.content_cleaned() | add_preview }}
</div>
{% endif %}
<footer>
<a href="{{ context.permalink }}">{{ context.published }}</a>
</footer>
</article>
{% endfor %}
<article>
<header>
{% if entry.author_photo %}
<img src="{{entry.author_photo}}"/>
{% endif %}
{% if entry.author_name %}
{{ entry.author_name }} -
{% endif %}
{{ entry.feed.name }}
</header>
{% if entry.title %}
<h1>{{ entry.title }}</h1>
{% endif %}
{% if entry.content %}
<div>
{{ entry.content_cleaned() | add_preview }}
</div>
{% endif %}
<footer>
<a href="{{ entry.permalink }}">{{ entry.published }}</a>
<div class="reply-area">
{% include '_reply.jinja2' with context %}
</div>
</footer>
</article>

View file

@ -1,6 +1,7 @@
{% extends "base.jinja2" %} {% extends "base.jinja2" %}
{% block head %} {% block head %}
<script src="{{url_for('static', filename='feed.js', version='2015-02-19')}}"></script> <script src="{{url_for('static', filename='feed.js', version='2015-02-19')}}"></script>
<script src="{{ url_for('static', filename='websocket_client.js', version='2015-02-22') }}"></script>
{% if current_user and current_user.settings {% if current_user and current_user.settings
and current_user.settings.get('reply-method') == 'indie-config' %} and current_user.settings.get('reply-method') == 'indie-config' %}
@ -21,56 +22,7 @@
{% block body %} {% block body %}
{% for entry in entries %} {% for entry in entries %}
{% for context in entry.reply_context %} {% include '_entry.jinja2' with context %}
<article class="reply-context">
<header>
{% if context.author_photo %}
<img src="{{context.author_photo}}"/>
{% endif %}
{% if context.author_name %}
{{ context.author_name }} -
{% endif %}
{{ context.permalink | domain_for_url }}
</header>
{% if context.title %}
<h1>{{ context.title }}</h1>
{% endif %}
{% if context.content %}
<div>
{{ context.content_cleaned() | add_preview }}
</div>
{% endif %}
<footer>
<a href="{{ context.permalink }}">{{ context.published }}</a>
</footer>
</article>
{% endfor %}
<article>
<header>
{% if entry.author_photo %}
<img src="{{entry.author_photo}}"/>
{% endif %}
{% if entry.author_name %}
{{ entry.author_name }} -
{% endif %}
{{ entry.feed.name }}
</header>
{% if entry.title %}
<h1>{{ entry.title }}</h1>
{% endif %}
{% if entry.content %}
<div>
{{ entry.content_cleaned() | add_preview }}
</div>
{% endif %}
<footer>
<a href="{{ entry.permalink }}">{{ entry.published }}</a>
<div class="reply-area">
{% include '_reply.jinja2' with context %}
</div>
</footer>
</article>
{% endfor %} {% endfor %}
{% if entries %} {% if entries %}
@ -79,4 +31,10 @@
</div> </div>
{% endif %} {% endif %}
{% if current_user %}
<script>
webSocketSubscribe('woodwind:user:' + {{ current_user.id }});
</script>
{% endif %}
{% endblock body %} {% endblock body %}