add realtime update notification
This commit is contained in:
parent
f8eee5261a
commit
52a8564b97
6 changed files with 145 additions and 55 deletions
32
websocket_server.js
Normal file
32
websocket_server.js
Normal 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);
|
|
@ -66,7 +66,5 @@ $(function(){
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
attachListeners();
|
attachListeners();
|
||||||
});
|
});
|
||||||
|
|
23
woodwind/static/websocket_client.js
Normal file
23
woodwind/static/websocket_client.js
Normal 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);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
50
woodwind/templates/_entry.jinja2
Normal file
50
woodwind/templates/_entry.jinja2
Normal 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>
|
|
@ -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 %}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue