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();
});

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 contextlib import contextmanager
from woodwind.models import Feed, Entry
from redis import StrictRedis
import bs4
import celery
import celery.utils.log
import datetime
import feedparser
import json
import mf2py
import mf2util
import re
@ -27,6 +29,8 @@ logger = celery.utils.log.get_task_logger(__name__)
engine = sqlalchemy.create_engine(Config.SQLALCHEMY_DATABASE_URI)
Session = sqlalchemy.orm.sessionmaker(bind=engine)
redis = StrictRedis()
@contextmanager
def session_scope():
@ -65,7 +69,7 @@ def update_feed(feed_id):
def process_feed(session, feed):
now = datetime.datetime.utcnow()
found_new = False
new_entries = []
try:
logger.info('fetching feed: %s', 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', []):
fetch_reply_context.delay(entry.id, in_reply_to)
found_new = True
new_entries.append(entry)
else:
logger.info('skipping previously seen post %s', old.permalink)
finally:
feed.last_checked = now
if found_new:
if new_entries:
feed.last_updated = now
session.commit()
if new_entries:
notify_feed_updated(session, feed, new_entries)
def check_push_subscription(session, feed, response):
@ -168,6 +175,28 @@ def check_push_subscription(session, feed, response):
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):
"""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

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" %}
{% block head %}
<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
and current_user.settings.get('reply-method') == 'indie-config' %}
@ -21,56 +22,7 @@
{% block body %}
{% for entry in entries %}
{% 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>
{% include '_entry.jinja2' with context %}
{% endfor %}
{% if entries %}
@ -79,4 +31,10 @@
</div>
{% endif %}
{% if current_user %}
<script>
webSocketSubscribe('woodwind:user:' + {{ current_user.id }});
</script>
{% endif %}
{% endblock body %}