port websocket_server from Node to Tornado
This commit is contained in:
parent
5ae94c2273
commit
6efa7f8d1d
7 changed files with 96 additions and 34 deletions
2
setup.py
2
setup.py
|
@ -12,4 +12,4 @@ setup(name='Woodwind',
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'Flask', 'Flask-Login', 'Flask-Micropub', 'Flask-SQLAlchemy',
|
'Flask', 'Flask-Login', 'Flask-Micropub', 'Flask-SQLAlchemy',
|
||||||
'beautifulsoup4', 'bleach', 'celery', 'feedparser', 'html5lib',
|
'beautifulsoup4', 'bleach', 'celery', 'feedparser', 'html5lib',
|
||||||
'mf2py', 'mf2util', 'redis', 'requests'])
|
'mf2py', 'mf2util', 'redis', 'requests', 'tornado'])
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
$(function(){
|
$(function(){
|
||||||
|
|
||||||
function clickOlderLink(evt) {
|
function clickOlderLink(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
$.get(this.href, function(result) {
|
$.get(this.href, function(result) {
|
||||||
|
@ -66,5 +67,30 @@ $(function(){
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// topic will be user:id or 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);
|
||||||
|
});
|
||||||
attachListeners();
|
attachListeners();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attachListeners();
|
||||||
|
if (WS_TOPIC) {
|
||||||
|
webSocketSubscribe(WS_TOPIC);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
|
|
||||||
// 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);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -194,7 +194,7 @@ def notify_feed_updated(session, feed, entries):
|
||||||
for e in entries
|
for e in entries
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
redis.publish('woodwind:user:{}'.format(user.id), message)
|
redis.publish('woodwind_notify', message)
|
||||||
|
|
||||||
|
|
||||||
def is_content_equal(e1, e2):
|
def is_content_equal(e1, e2):
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
{% 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='websocket_client.js', version='2015-02-22') }}"></script>
|
{% if ws_topic %}
|
||||||
|
<script>var WS_TOPIC = "{{ ws_topic }}";</script>
|
||||||
|
{% endif %}
|
||||||
|
<script src="{{url_for('static', filename='feed.js', version='2015-02-24')}}"></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' %}
|
||||||
|
@ -31,10 +34,4 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if current_user.is_authenticated() %}
|
|
||||||
<script>
|
|
||||||
webSocketSubscribe('woodwind:user:' + {{ current_user.id }});
|
|
||||||
</script>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock body %}
|
{% endblock body %}
|
||||||
|
|
|
@ -21,6 +21,8 @@ views = flask.Blueprint('views', __name__)
|
||||||
def index():
|
def index():
|
||||||
page = int(flask.request.args.get('page', 1))
|
page = int(flask.request.args.get('page', 1))
|
||||||
entries = []
|
entries = []
|
||||||
|
ws_topic = None
|
||||||
|
|
||||||
if flask_login.current_user.is_authenticated():
|
if flask_login.current_user.is_authenticated():
|
||||||
per_page = flask.current_app.config.get('PER_PAGE', 30)
|
per_page = flask.current_app.config.get('PER_PAGE', 30)
|
||||||
offset = (page - 1) * per_page
|
offset = (page - 1) * per_page
|
||||||
|
@ -32,12 +34,19 @@ def index():
|
||||||
if 'feed' in flask.request.args:
|
if 'feed' in flask.request.args:
|
||||||
feed_hex = flask.request.args.get('feed').encode()
|
feed_hex = flask.request.args.get('feed').encode()
|
||||||
feed_url = binascii.unhexlify(feed_hex).decode('utf-8')
|
feed_url = binascii.unhexlify(feed_hex).decode('utf-8')
|
||||||
|
feed = Feed.query.filter_by(feed=feed_url).first()
|
||||||
|
if not feed:
|
||||||
|
flask.abort(404)
|
||||||
entry_query = entry_query.filter(Feed.feed == feed_url)
|
entry_query = entry_query.filter(Feed.feed == feed_url)
|
||||||
|
ws_topic = 'feed:{}'.format(feed.id)
|
||||||
|
else:
|
||||||
|
ws_topic = 'user:{}'.format(flask_login.current_user.id)
|
||||||
|
|
||||||
entries = entry_query.order_by(Entry.published.desc())\
|
entries = entry_query.order_by(Entry.published.desc())\
|
||||||
.offset(offset).limit(per_page).all()
|
.offset(offset).limit(per_page).all()
|
||||||
|
|
||||||
return flask.render_template('feed.jinja2', entries=entries, page=page)
|
return flask.render_template('feed.jinja2', entries=entries, page=page,
|
||||||
|
ws_topic=ws_topic)
|
||||||
|
|
||||||
|
|
||||||
@views.route('/install')
|
@views.route('/install')
|
||||||
|
|
53
woodwind/websocket_server.py
Normal file
53
woodwind/websocket_server.py
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import itertools
|
||||||
|
import json
|
||||||
|
import redis
|
||||||
|
import threading
|
||||||
|
import tornado.ioloop
|
||||||
|
import tornado.websocket
|
||||||
|
|
||||||
|
|
||||||
|
SUBSCRIBERS = {}
|
||||||
|
|
||||||
|
|
||||||
|
def redis_loop():
|
||||||
|
r = redis.StrictRedis()
|
||||||
|
ps = r.pubsub()
|
||||||
|
ps.subscribe('woodwind_notify')
|
||||||
|
for message in ps.listen():
|
||||||
|
if message['type'] == 'message':
|
||||||
|
msg_data = str(message.get('data'), 'utf-8')
|
||||||
|
msg_blob = json.loads(msg_data)
|
||||||
|
user_topic = 'user:{}'.format(msg_blob.get('user'))
|
||||||
|
feed_topic = 'feed:{}'.format(msg_blob.get('feed'))
|
||||||
|
for subscriber in itertools.chain(
|
||||||
|
SUBSCRIBERS.get(user_topic, []),
|
||||||
|
SUBSCRIBERS.get(feed_topic, [])):
|
||||||
|
tornado.ioloop.IOLoop.instance().add_callback(
|
||||||
|
lambda: subscriber.forward_message(msg_data))
|
||||||
|
|
||||||
|
|
||||||
|
class WSHandler(tornado.websocket.WebSocketHandler):
|
||||||
|
def check_origin(self, origin):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def forward_message(self, message):
|
||||||
|
self.write_message(message)
|
||||||
|
|
||||||
|
def on_message(self, topic):
|
||||||
|
self.topic = topic
|
||||||
|
SUBSCRIBERS.setdefault(topic, []).append(self)
|
||||||
|
|
||||||
|
def on_close(self):
|
||||||
|
if hasattr(self, 'topic'):
|
||||||
|
SUBSCRIBERS.setdefault(self.topic, []).append(self)
|
||||||
|
|
||||||
|
|
||||||
|
application = tornado.web.Application([
|
||||||
|
(r'/', WSHandler),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
threading.Thread(target=redis_loop).start()
|
||||||
|
application.listen(8077)
|
||||||
|
tornado.ioloop.IOLoop.instance().start()
|
Loading…
Add table
Add a link
Reference in a new issue