diff --git a/woodwind/api.py b/woodwind/api.py index 1ddfefd..3c1860b 100644 --- a/woodwind/api.py +++ b/woodwind/api.py @@ -16,9 +16,14 @@ def publish(): data = { 'h': 'entry', 'syndicate-to[]': syndicate_to, + 'access_token': flask_login.current_user.access_token, } - if action == 'like': + if action.startswith('rsvp-'): + data['in-reply-to'] = target + data['content'] = content + data['rsvp'] = action.split('-', 1)[-1] + elif action == 'like': data['like-of'] = target elif action == 'repost': data['repost-of'] = target diff --git a/woodwind/app.py b/woodwind/app.py index 5b35112..27bde0e 100644 --- a/woodwind/app.py +++ b/woodwind/app.py @@ -36,7 +36,11 @@ def configure_logging(app): return app.logger.setLevel(logging.DEBUG) - app.logger.addHandler(logging.StreamHandler(sys.stdout)) + + handler = logging.StreamHandler(sys.stdout) + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + handler.setFormatter(formatter) + app.logger.addHandler(handler) recipients = app.config.get('ADMIN_EMAILS') if recipients: diff --git a/woodwind/push.py b/woodwind/push.py index 2a2c3c0..4645c61 100644 --- a/woodwind/push.py +++ b/woodwind/push.py @@ -31,13 +31,14 @@ def notify(feed_id): if not feed: current_app.logger.warn( 'could not find feed corresponding to %d', feed_id) - abort(404) + return make_response('no feed with id %d' % feed_id, 400) if topic != feed.push_topic: current_app.logger.warn( 'feed topic (%s) does not match subscription request (%s)', feed.push_topic, topic) - abort(404) + return make_response( + 'topic %s does not match subscription request %s' % (feed.push_topic, topic), 400) current_app.logger.debug( 'PuSH verify subscribe for feed=%r, topic=%s', feed, topic) @@ -48,18 +49,28 @@ def notify(feed_id): db.session.commit() return challenge - elif mode == 'unsubscribe' and (not feed or topic != feed.push_topic): - current_app.logger.debug( - 'PuSH verify unsubscribe for feed=%r, topic=%s', feed, topic) - return challenge - current_app.logger.debug('PuSH cannot verify %s for feed=%r, topic=%s', - mode, feed, topic) - abort(404) + elif mode == 'unsubscribe': + if not feed or topic != feed.push_topic: + current_app.logger.debug( + 'PuSH verify unsubscribe for feed=%r, topic=%s', feed, topic) + return challenge + else: + current_app.logger.debug( + 'PuSH denying unsubscribe for feed=%r, topic=%s', feed, topic) + return make_response('unsubscribe denied', 400) + + elif mode: + current_app.logger.debug('PuSH request with unknown mode %s', mode) + return make_response('unrecognized hub.mode=%s' % mode, 400) + + else: + current_app.logger.debug('PuSH request with no mode') + return make_response('missing requred parameter hub.mode', 400) if not feed: current_app.logger.warn( 'could not find feed corresponding to %d', feed_id) - abort(404) + return make_response('no feed with id %d' % feed_id, 400) # could it be? an actual push notification!? current_app.logger.debug( @@ -83,7 +94,7 @@ def notify(feed_id): content = request.data.decode('utf-8') tasks.q_high.enqueue(tasks.update_feed, feed.id, - content=content, content_type=content_type, + content=content, content_type=content_type, is_polling=False) feed.last_pinged = datetime.datetime.utcnow() db.session.commit() diff --git a/woodwind/static/feed.js b/woodwind/static/feed.js index cff565f..a3ac69f 100644 --- a/woodwind/static/feed.js +++ b/woodwind/static/feed.js @@ -39,20 +39,30 @@ $(function(){ function submitMicropubForm(evt) { evt.preventDefault(); - var form = $(this).closest('form'); + var button = this; + var form = $(button).closest('form'); var replyArea = form.parent(); var endpoint = form.attr('action'); var responseArea = $('.micropub-response', replyArea); var formData = form.serializeArray(); - formData.push({name: this.name, value: this.value}); + formData.push({name: button.name, value: button.value}); $.post( - form.attr('action'), + endpoint, formData, function(result) { if (Math.floor(result.code / 100) == 2) { responseArea.html('Success!'); - $(".micropub-form textarea").val(""); + $("textarea", form).val(""); + + if (button.value === 'rsvp-yes') { + $(".rsvps", form).html('✓ Going'); + } else if (button.value === 'rsvp-maybe') { + $(".rsvps", form).html('? Interested'); + } else if (button.value === 'rsvp-no') { + $(".rsvps", form).html('✗ Not Going'); + } + } else { responseArea.html('Failure'); } diff --git a/woodwind/static/style.css b/woodwind/static/style.css index 11386c2..d5c1179 100644 --- a/woodwind/static/style.css +++ b/woodwind/static/style.css @@ -361,7 +361,7 @@ th { body { font: 12pt Helvetica, Arial, sans-serif; line-height: 1.4em; - background: #ECEBF0; + background: #ecebf0; /*background: #f4f4f4;*/ padding-top: 1em; } @@ -377,13 +377,17 @@ p:first-child { p:last-child { margin-bottom: 0; } -header, main { +header, main, .footer { max-width: 800px; margin: 0 auto; } header { margin-bottom: 1em; } +.footer { + font-size: 0.8em; + text-align: center; } + ul#navigation { list-style-type: none; margin: 0; @@ -399,8 +403,8 @@ ul#navigation { .button-link a { padding: 0.5em; background-color: #353129; - color: #ECEBF0; - border: 1px solid #ECEBF0; + color: #ecebf0; + border: 1px solid #ecebf0; border-radius: 4px; display: inline-block; } @@ -412,7 +416,7 @@ ul#navigation { article { margin-bottom: 1em; - box-shadow: 0 0 2px #687D77; + box-shadow: 0 0 2px #687d77; background-color: white; border-radius: 3px; padding: 0.5em; } @@ -434,8 +438,8 @@ article { article img, article video { max-width: 100%; } article header { - color: #484A47; - border-bottom: 1px solid #687D77; + color: #484a47; + border-bottom: 1px solid #687d77; margin-bottom: 0.5em; overflow: auto; } article header img { @@ -494,14 +498,17 @@ button { margin: 0; vertical-align: middle; } .micropub-form button { - height: 24px; - width: 24px; - padding: 4px; background-color: #eee; border-radius: 3px; - border: 0; - line-height: 1; - vertical-align: middle; } + vertical-align: middle; + border: 0; } + .micropub-form button.small { + width: 24px; + height: 24px; + padding: 4px; + line-height: 1; } +.micropub-form .rsvps { + text-align: center; } .syndication-toggle { display: inline-block; } @@ -531,11 +538,11 @@ button { .reply-area .reply-link { display: inline-block; padding: 0.2em; - border: 1px solid #687D77; + border: 1px solid #687d77; border-radius: 4px; - background-color: #ECEBF0; + background-color: #ecebf0; text-decoration: none; - color: #484A47; + color: #484a47; min-width: 50px; text-align: center; } @@ -548,5 +555,3 @@ button { max-height: 1.2em; min-width: inherit; min-height: inherit; } } - -/*# sourceMappingURL=style.css.map */ diff --git a/woodwind/static/style.css.map b/woodwind/static/style.css.map index 5644fa3..afe393b 100644 --- a/woodwind/static/style.css.map +++ b/woodwind/static/style.css.map @@ -1,6 +1,6 @@ { "version": 3, -"mappings": "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", +"mappings": "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", "sources": ["normalize.scss","style.scss"], "names": [], "file": "style.css" diff --git a/woodwind/static/style.scss b/woodwind/static/style.scss index c7abbc6..1a08b4b 100644 --- a/woodwind/static/style.scss +++ b/woodwind/static/style.scss @@ -43,7 +43,7 @@ p { } } -header, main { +header, main, .footer { max-width: 800px; margin: 0 auto; } @@ -52,6 +52,11 @@ header { margin-bottom: 1em; } +.footer { + font-size: 0.8em; + text-align: center; +} + ul#navigation { list-style-type: none; margin: 0; @@ -201,13 +206,20 @@ button { } button { - height: 24px;width: 24px; - padding: 4px; background-color: #eee; border-radius: 3px; - border: 0; - line-height: 1; vertical-align: middle; + border: 0; + &.small { + width: 24px; + height: 24px; + padding: 4px; + line-height: 1; + } + } + + .rsvps { + text-align: center; } } diff --git a/woodwind/tasks.py b/woodwind/tasks.py index c6c60f7..fcd23ec 100644 --- a/woodwind/tasks.py +++ b/woodwind/tasks.py @@ -1,9 +1,10 @@ from contextlib import contextmanager -from flask import current_app +from flask import current_app, url_for from redis import StrictRedis from woodwind import util from woodwind.extensions import db from woodwind.models import Feed, Entry +import sqlalchemy import bs4 import datetime import feedparser @@ -16,6 +17,7 @@ import requests import rq import sys import time +import traceback import urllib.parse # normal update interval for polling feeds @@ -112,12 +114,12 @@ def update_feed(feed_id, content=None, with flask_app() as app: feed = Feed.query.get(feed_id) - current_app.logger.info('Updating {}'.format(feed)) + current_app.logger.info('Updating {}'.format(str(feed)[:32])) now = datetime.datetime.utcnow() - new_ids = [] - updated_ids = [] + new_entries = [] + updated_entries = [] reply_pairs = [] try: @@ -125,7 +127,7 @@ def update_feed(feed_id, content=None, current_app.logger.info('using provided content. size=%d', len(content)) else: - current_app.logger.info('fetching feed: %s', feed) + current_app.logger.info('fetching feed: %s', str(feed)[:32]) try: response = util.requests_get(feed.feed) @@ -163,24 +165,30 @@ def update_feed(feed_id, content=None, result = [] for entry in result: + current_app.logger.debug('searching for entry with uid=%s', entry.uid) old = Entry.query\ .filter(Entry.feed == feed)\ .filter(Entry.uid == entry.uid)\ .order_by(Entry.id.desc())\ .first() + current_app.logger.debug('done searcing: %s', 'found' if old else 'not found') + # have we seen this post before if not old: + current_app.logger.debug('this is a new post, saving a new entry') # set a default value for published if none is provided entry.published = entry.published or now in_reply_tos = entry.get_property('in-reply-to', []) + db.session.add(entry) feed.entries.append(entry) - db.session.commit() - new_ids.append(entry.id) + new_entries.append(entry) for irt in in_reply_tos: - reply_pairs.append((entry.id, irt)) + reply_pairs.append((entry, irt)) elif not is_content_equal(old, entry): + current_app.logger.debug('this post content has changed, updating entry') + entry.published = entry.published or old.published in_reply_tos = entry.get_property('in-reply-to', []) # we're updating an old entriy, use the original @@ -190,46 +198,48 @@ def update_feed(feed_id, content=None, # punt on deleting for now, learn about cascade # and stuff later # session.delete(old) + db.session.add(entry) feed.entries.append(entry) - db.session.commit() - updated_ids.append(entry.id) + updated_entries.append(entry) for irt in in_reply_tos: - reply_pairs.append((entry.id, irt)) + reply_pairs.append((entry, irt)) else: current_app.logger.debug( 'skipping previously seen post %s', old.permalink) - for entry_id, in_reply_to in reply_pairs: - fetch_reply_context(entry_id, in_reply_to, now) + for entry, in_reply_to in reply_pairs: + fetch_reply_context(entry, in_reply_to, now) + + db.session.commit() + except: + db.session.rollback() + raise finally: if is_polling: feed.last_checked = now - if new_ids or updated_ids: + if new_entries or updated_entries: feed.last_updated = now db.session.commit() - if new_ids: - notify_feed_updated(app, feed_id, new_ids) + + if new_entries: + notify_feed_updated(app, feed_id, new_entries) def check_push_subscription(feed, response): - def build_callback_url(): - return '{}://{}/_notify/{}'.format( - getattr(current_app.config, 'PREFERRED_URL_SCHEME', 'http'), - current_app.config['SERVER_NAME'], - feed.id) - def send_request(mode, hub, topic): hub = urllib.parse.urljoin(feed.feed, hub) topic = urllib.parse.urljoin(feed.feed, topic) + callback = url_for('push.notify', feed_id=feed.id, _external=True) current_app.logger.debug( - 'sending %s request for hub=%r, topic=%r', mode, hub, topic) + 'sending %s request for hub=%r, topic=%r, callback=%r', + mode, hub, topic, callback) r = requests.post(hub, data={ 'hub.mode': mode, 'hub.topic': topic, - 'hub.callback': build_callback_url(), + 'hub.callback': callback, 'hub.secret': feed.get_or_create_push_secret(), 'hub.verify': 'sync', # backcompat with 0.3 }) @@ -267,6 +277,8 @@ def check_push_subscription(feed, response): if ((expiry and expiry - datetime.datetime.utcnow() <= UPDATE_INTERVAL_PUSH) or hub != old_hub or topic != old_topic or not feed.push_verified): + current_app.logger.debug('push subscription expired or hub/topic changed') + feed.push_hub = hub feed.push_topic = topic feed.push_verified = False @@ -274,29 +286,24 @@ def check_push_subscription(feed, response): db.session.commit() if old_hub and old_topic and hub != old_hub and topic != old_topic: + current_app.logger.debug('unsubscribing hub=%s, topic=%s', old_hub, old_topic) send_request('unsubscribe', old_hub, old_topic) if hub and topic: + current_app.logger.debug('subscribing hub=%s, topic=%s', hub, topic) send_request('subscribe', hub, topic) db.session.commit() -def notify_feed_updated(app, feed_id, entry_ids): +def notify_feed_updated(app, feed_id, entries): """Render the new entries and publish them to redis """ from flask import render_template import flask.ext.login as flask_login - current_app.logger.debug( - 'notifying feed updated for entries %r', entry_ids) + current_app.logger.debug('notifying feed updated: %s', feed_id) feed = Feed.query.get(feed_id) - entries = Entry.query\ - .filter(Entry.id.in_(entry_ids))\ - .order_by(Entry.retrieved.desc(), - Entry.published.desc())\ - .all() - for s in feed.subscriptions: with app.test_request_context(): flask_login.login_user(s.user, remember=True) @@ -336,16 +343,18 @@ def is_content_equal(e1, e2): content = COMMENT_RE.sub('', content) return content - return (e1.title == e2.title - and normalize(e1.content) == normalize(e2.content) - and e1.author_name == e2.author_name - and e1.author_url == e2.author_url - and e1.author_photo == e2.author_photo - and e1.properties == e2.properties) + return ( + e1.title == e2.title + and normalize(e1.content) == normalize(e2.content) + and e1.author_name == e2.author_name + and e1.author_url == e2.author_url + and e1.author_photo == e2.author_photo + and e1.properties == e2.properties + ) def process_xml_feed_for_new_entries(feed, content, backfill, now): - current_app.logger.debug('fetching xml feed: %s', feed) + current_app.logger.debug('fetching xml feed: %s', str(feed)[:32]) parsed = feedparser.parse(content, response_headers={ 'content-location': feed.feed, }) @@ -354,12 +363,11 @@ def process_xml_feed_for_new_entries(feed, content, backfill, now): default_author_name = feed_props.get('author_detail', {}).get('name') default_author_photo = feed_props.get('logo') - current_app.logger.debug('found {} entries'.format(len(parsed.entries))) + current_app.logger.debug('found %d entries', len(parsed.entries)) # work from the bottom up (oldest first, usually) for p_entry in reversed(parsed.entries): - current_app.logger.debug('processing entry {}'.format( - str(p_entry)[:256])) + current_app.logger.debug('processing entry %s', str(p_entry)[:32]) permalink = p_entry.get('link') uid = p_entry.get('id') or permalink @@ -406,6 +414,8 @@ def process_xml_feed_for_new_entries(feed, content, backfill, now): video = VIDEO_ENCLOSURE_TMPL.format(href=link.get('href')) content = (content or '') + video + current_app.logger.debug('building entry') + entry = Entry( published=published, updated=updated, @@ -422,6 +432,8 @@ def process_xml_feed_for_new_entries(feed, content, backfill, now): author_photo=default_author_photo or fallback_photo(feed.origin)) + current_app.logger.debug('yielding entry') + yield entry @@ -462,7 +474,7 @@ def hentry_to_entry(hentry, feed, backfill, now): title = hentry.get('name') content = hentry.get('content') - if not content: + if not content and hentry.get('type') == 'entry': content = title title = None @@ -518,6 +530,16 @@ def hentry_to_entry(hentry, feed, backfill, now): if value: entry.set_property(prop, value) + if 'start-str' in hentry: + entry.set_property('start', hentry.get('start-str')) + + if 'end-str' in hentry: + entry.set_property('end', hentry.get('end-str')) + + # set a flag for events so we can show RSVP buttons + if hentry.get('type') == 'event': + entry.set_property('event', True) + # does it look like a jam? plain = hentry.get('content-plain') if plain and JAM_RE.match(plain): @@ -527,25 +549,28 @@ def hentry_to_entry(hentry, feed, backfill, now): return entry -def fetch_reply_context(entry_id, in_reply_to, now): - with flask_app(): - entry = Entry.query.get(entry_id) - context = Entry.query\ - .join(Entry.feed)\ - .filter(Entry.permalink==in_reply_to, Feed.type == 'html')\ - .first() +def fetch_reply_context(entry, in_reply_to, now): + context = Entry.query\ + .join(Entry.feed)\ + .filter(Entry.permalink==in_reply_to, Feed.type == 'html')\ + .first() - if not context: - current_app.logger.info('fetching in-reply-to url: %s', - in_reply_to) + if not context: + current_app.logger.info('fetching in-reply-to: %s', in_reply_to) + try: + proxied_reply_url = proxy_url(in_reply_to) parsed = mf2util.interpret( - mf2py.parse(url=proxy_url(in_reply_to)), in_reply_to) + mf2py.parse(url=proxied_reply_url), in_reply_to) if parsed: context = hentry_to_entry(parsed, None, False, now) + except requests.exceptions.RequestException as err: + current_app.logger.warn( + '%s fetching reply context: %s for entry: %s', + type(err).__name__, proxied_reply_url, entry.permalink) - if context: - entry.reply_context.append(context) - db.session.commit() + if context: + db.session.add(context) + entry.reply_context.append(context) def proxy_url(url): diff --git a/woodwind/templates/_entry.jinja2 b/woodwind/templates/_entry.jinja2 index 62a2c9f..abd3f57 100644 --- a/woodwind/templates/_entry.jinja2 +++ b/woodwind/templates/_entry.jinja2 @@ -46,6 +46,18 @@

{{ entry.title }}

{% endif %} + {% if entry.get_property('event') %} +

+ {% if entry.get_property('start') %} + start: {{ entry.get_property('start') }} + {% endif %} +
+ {% if entry.get_property('end') %} + end: {{ entry.get_property('end') }} + {% endif %} +

+ {% endif %} + {% set photo = entry.get_property('photo') %} {% if photo and (not entry.content or ' diff --git a/woodwind/templates/_reply.jinja2 b/woodwind/templates/_reply.jinja2 index ab71317..fc30ae4 100644 --- a/woodwind/templates/_reply.jinja2 +++ b/woodwind/templates/_reply.jinja2 @@ -4,11 +4,20 @@ {% if reply_method == 'micropub' and current_user.micropub_endpoint %}
+ + {% if entry.get_property('event') %} +
+ + + +
+ {% endif %} +
- - - + + +