Merge branch 'master' into requires-io-master

This commit is contained in:
Kyle Mahan 2016-04-21 13:22:54 -07:00
commit c85430e071
14 changed files with 240 additions and 112 deletions

View file

@ -16,9 +16,14 @@ def publish():
data = { data = {
'h': 'entry', 'h': 'entry',
'syndicate-to[]': syndicate_to, '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 data['like-of'] = target
elif action == 'repost': elif action == 'repost':
data['repost-of'] = target data['repost-of'] = target

View file

@ -36,7 +36,11 @@ def configure_logging(app):
return return
app.logger.setLevel(logging.DEBUG) 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') recipients = app.config.get('ADMIN_EMAILS')
if recipients: if recipients:

View file

@ -31,13 +31,14 @@ def notify(feed_id):
if not feed: if not feed:
current_app.logger.warn( current_app.logger.warn(
'could not find feed corresponding to %d', feed_id) '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: if topic != feed.push_topic:
current_app.logger.warn( current_app.logger.warn(
'feed topic (%s) does not match subscription request (%s)', 'feed topic (%s) does not match subscription request (%s)',
feed.push_topic, topic) 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( current_app.logger.debug(
'PuSH verify subscribe for feed=%r, topic=%s', feed, topic) 'PuSH verify subscribe for feed=%r, topic=%s', feed, topic)
@ -48,18 +49,28 @@ def notify(feed_id):
db.session.commit() db.session.commit()
return challenge return challenge
elif mode == 'unsubscribe' and (not feed or topic != feed.push_topic): elif mode == 'unsubscribe':
current_app.logger.debug( if not feed or topic != feed.push_topic:
'PuSH verify unsubscribe for feed=%r, topic=%s', feed, topic) current_app.logger.debug(
return challenge 'PuSH verify unsubscribe for feed=%r, topic=%s', feed, topic)
current_app.logger.debug('PuSH cannot verify %s for feed=%r, topic=%s', return challenge
mode, feed, topic) else:
abort(404) 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: if not feed:
current_app.logger.warn( current_app.logger.warn(
'could not find feed corresponding to %d', feed_id) '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!? # could it be? an actual push notification!?
current_app.logger.debug( current_app.logger.debug(
@ -83,7 +94,7 @@ def notify(feed_id):
content = request.data.decode('utf-8') content = request.data.decode('utf-8')
tasks.q_high.enqueue(tasks.update_feed, feed.id, tasks.q_high.enqueue(tasks.update_feed, feed.id,
content=content, content_type=content_type, content=content, content_type=content_type,
is_polling=False) is_polling=False)
feed.last_pinged = datetime.datetime.utcnow() feed.last_pinged = datetime.datetime.utcnow()
db.session.commit() db.session.commit()

View file

@ -39,20 +39,30 @@ $(function(){
function submitMicropubForm(evt) { function submitMicropubForm(evt) {
evt.preventDefault(); evt.preventDefault();
var form = $(this).closest('form'); var button = this;
var form = $(button).closest('form');
var replyArea = form.parent(); var replyArea = form.parent();
var endpoint = form.attr('action'); var endpoint = form.attr('action');
var responseArea = $('.micropub-response', replyArea); var responseArea = $('.micropub-response', replyArea);
var formData = form.serializeArray(); var formData = form.serializeArray();
formData.push({name: this.name, value: this.value}); formData.push({name: button.name, value: button.value});
$.post( $.post(
form.attr('action'), endpoint,
formData, formData,
function(result) { function(result) {
if (Math.floor(result.code / 100) == 2) { if (Math.floor(result.code / 100) == 2) {
responseArea.html('<a target="_blank" href="' + result.location + '">Success!</a>'); responseArea.html('<a target="_blank" href="' + result.location + '">Success!</a>');
$(".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 { } else {
responseArea.html('Failure'); responseArea.html('Failure');
} }

View file

@ -361,7 +361,7 @@ th {
body { body {
font: 12pt Helvetica, Arial, sans-serif; font: 12pt Helvetica, Arial, sans-serif;
line-height: 1.4em; line-height: 1.4em;
background: #ECEBF0; background: #ecebf0;
/*background: #f4f4f4;*/ /*background: #f4f4f4;*/
padding-top: 1em; } padding-top: 1em; }
@ -377,13 +377,17 @@ p:first-child {
p:last-child { p:last-child {
margin-bottom: 0; } margin-bottom: 0; }
header, main { header, main, .footer {
max-width: 800px; max-width: 800px;
margin: 0 auto; } margin: 0 auto; }
header { header {
margin-bottom: 1em; } margin-bottom: 1em; }
.footer {
font-size: 0.8em;
text-align: center; }
ul#navigation { ul#navigation {
list-style-type: none; list-style-type: none;
margin: 0; margin: 0;
@ -399,8 +403,8 @@ ul#navigation {
.button-link a { .button-link a {
padding: 0.5em; padding: 0.5em;
background-color: #353129; background-color: #353129;
color: #ECEBF0; color: #ecebf0;
border: 1px solid #ECEBF0; border: 1px solid #ecebf0;
border-radius: 4px; border-radius: 4px;
display: inline-block; } display: inline-block; }
@ -412,7 +416,7 @@ ul#navigation {
article { article {
margin-bottom: 1em; margin-bottom: 1em;
box-shadow: 0 0 2px #687D77; box-shadow: 0 0 2px #687d77;
background-color: white; background-color: white;
border-radius: 3px; border-radius: 3px;
padding: 0.5em; } padding: 0.5em; }
@ -434,8 +438,8 @@ article {
article img, article video { article img, article video {
max-width: 100%; } max-width: 100%; }
article header { article header {
color: #484A47; color: #484a47;
border-bottom: 1px solid #687D77; border-bottom: 1px solid #687d77;
margin-bottom: 0.5em; margin-bottom: 0.5em;
overflow: auto; } overflow: auto; }
article header img { article header img {
@ -494,14 +498,17 @@ button {
margin: 0; margin: 0;
vertical-align: middle; } vertical-align: middle; }
.micropub-form button { .micropub-form button {
height: 24px;
width: 24px;
padding: 4px;
background-color: #eee; background-color: #eee;
border-radius: 3px; border-radius: 3px;
border: 0; vertical-align: middle;
line-height: 1; border: 0; }
vertical-align: middle; } .micropub-form button.small {
width: 24px;
height: 24px;
padding: 4px;
line-height: 1; }
.micropub-form .rsvps {
text-align: center; }
.syndication-toggle { .syndication-toggle {
display: inline-block; } display: inline-block; }
@ -531,11 +538,11 @@ button {
.reply-area .reply-link { .reply-area .reply-link {
display: inline-block; display: inline-block;
padding: 0.2em; padding: 0.2em;
border: 1px solid #687D77; border: 1px solid #687d77;
border-radius: 4px; border-radius: 4px;
background-color: #ECEBF0; background-color: #ecebf0;
text-decoration: none; text-decoration: none;
color: #484A47; color: #484a47;
min-width: 50px; min-width: 50px;
text-align: center; } text-align: center; }
@ -548,5 +555,3 @@ button {
max-height: 1.2em; max-height: 1.2em;
min-width: inherit; min-width: inherit;
min-height: inherit; } } min-height: inherit; } }
/*# sourceMappingURL=style.css.map */

File diff suppressed because one or more lines are too long

View file

@ -43,7 +43,7 @@ p {
} }
} }
header, main { header, main, .footer {
max-width: 800px; max-width: 800px;
margin: 0 auto; margin: 0 auto;
} }
@ -52,6 +52,11 @@ header {
margin-bottom: 1em; margin-bottom: 1em;
} }
.footer {
font-size: 0.8em;
text-align: center;
}
ul#navigation { ul#navigation {
list-style-type: none; list-style-type: none;
margin: 0; margin: 0;
@ -201,13 +206,20 @@ button {
} }
button { button {
height: 24px;width: 24px;
padding: 4px;
background-color: #eee; background-color: #eee;
border-radius: 3px; border-radius: 3px;
border: 0;
line-height: 1;
vertical-align: middle; vertical-align: middle;
border: 0;
&.small {
width: 24px;
height: 24px;
padding: 4px;
line-height: 1;
}
}
.rsvps {
text-align: center;
} }
} }

View file

@ -1,9 +1,10 @@
from contextlib import contextmanager from contextlib import contextmanager
from flask import current_app from flask import current_app, url_for
from redis import StrictRedis from redis import StrictRedis
from woodwind import util from woodwind import util
from woodwind.extensions import db from woodwind.extensions import db
from woodwind.models import Feed, Entry from woodwind.models import Feed, Entry
import sqlalchemy
import bs4 import bs4
import datetime import datetime
import feedparser import feedparser
@ -16,6 +17,7 @@ import requests
import rq import rq
import sys import sys
import time import time
import traceback
import urllib.parse import urllib.parse
# normal update interval for polling feeds # normal update interval for polling feeds
@ -112,12 +114,12 @@ def update_feed(feed_id, content=None,
with flask_app() as app: with flask_app() as app:
feed = Feed.query.get(feed_id) 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() now = datetime.datetime.utcnow()
new_ids = [] new_entries = []
updated_ids = [] updated_entries = []
reply_pairs = [] reply_pairs = []
try: try:
@ -125,7 +127,7 @@ def update_feed(feed_id, content=None,
current_app.logger.info('using provided content. size=%d', current_app.logger.info('using provided content. size=%d',
len(content)) len(content))
else: else:
current_app.logger.info('fetching feed: %s', feed) current_app.logger.info('fetching feed: %s', str(feed)[:32])
try: try:
response = util.requests_get(feed.feed) response = util.requests_get(feed.feed)
@ -163,24 +165,30 @@ def update_feed(feed_id, content=None,
result = [] result = []
for entry in result: for entry in result:
current_app.logger.debug('searching for entry with uid=%s', entry.uid)
old = Entry.query\ old = Entry.query\
.filter(Entry.feed == feed)\ .filter(Entry.feed == feed)\
.filter(Entry.uid == entry.uid)\ .filter(Entry.uid == entry.uid)\
.order_by(Entry.id.desc())\ .order_by(Entry.id.desc())\
.first() .first()
current_app.logger.debug('done searcing: %s', 'found' if old else 'not found')
# have we seen this post before # have we seen this post before
if not old: 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 # set a default value for published if none is provided
entry.published = entry.published or now entry.published = entry.published or now
in_reply_tos = entry.get_property('in-reply-to', []) in_reply_tos = entry.get_property('in-reply-to', [])
db.session.add(entry)
feed.entries.append(entry) feed.entries.append(entry)
db.session.commit()
new_ids.append(entry.id) new_entries.append(entry)
for irt in in_reply_tos: for irt in in_reply_tos:
reply_pairs.append((entry.id, irt)) reply_pairs.append((entry, irt))
elif not is_content_equal(old, entry): 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 entry.published = entry.published or old.published
in_reply_tos = entry.get_property('in-reply-to', []) in_reply_tos = entry.get_property('in-reply-to', [])
# we're updating an old entriy, use the original # 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 # punt on deleting for now, learn about cascade
# and stuff later # and stuff later
# session.delete(old) # session.delete(old)
db.session.add(entry)
feed.entries.append(entry) feed.entries.append(entry)
db.session.commit()
updated_ids.append(entry.id) updated_entries.append(entry)
for irt in in_reply_tos: for irt in in_reply_tos:
reply_pairs.append((entry.id, irt)) reply_pairs.append((entry, irt))
else: else:
current_app.logger.debug( current_app.logger.debug(
'skipping previously seen post %s', old.permalink) 'skipping previously seen post %s', old.permalink)
for entry_id, in_reply_to in reply_pairs: for entry, in_reply_to in reply_pairs:
fetch_reply_context(entry_id, in_reply_to, now) fetch_reply_context(entry, in_reply_to, now)
db.session.commit()
except:
db.session.rollback()
raise
finally: finally:
if is_polling: if is_polling:
feed.last_checked = now feed.last_checked = now
if new_ids or updated_ids: if new_entries or updated_entries:
feed.last_updated = now feed.last_updated = now
db.session.commit() 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 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): def send_request(mode, hub, topic):
hub = urllib.parse.urljoin(feed.feed, hub) hub = urllib.parse.urljoin(feed.feed, hub)
topic = urllib.parse.urljoin(feed.feed, topic) topic = urllib.parse.urljoin(feed.feed, topic)
callback = url_for('push.notify', feed_id=feed.id, _external=True)
current_app.logger.debug( 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={ r = requests.post(hub, data={
'hub.mode': mode, 'hub.mode': mode,
'hub.topic': topic, 'hub.topic': topic,
'hub.callback': build_callback_url(), 'hub.callback': callback,
'hub.secret': feed.get_or_create_push_secret(), 'hub.secret': feed.get_or_create_push_secret(),
'hub.verify': 'sync', # backcompat with 0.3 'hub.verify': 'sync', # backcompat with 0.3
}) })
@ -267,6 +277,8 @@ def check_push_subscription(feed, response):
if ((expiry and expiry - datetime.datetime.utcnow() if ((expiry and expiry - datetime.datetime.utcnow()
<= UPDATE_INTERVAL_PUSH) <= UPDATE_INTERVAL_PUSH)
or hub != old_hub or topic != old_topic or not feed.push_verified): 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_hub = hub
feed.push_topic = topic feed.push_topic = topic
feed.push_verified = False feed.push_verified = False
@ -274,29 +286,24 @@ def check_push_subscription(feed, response):
db.session.commit() db.session.commit()
if old_hub and old_topic and hub != old_hub and topic != old_topic: 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) send_request('unsubscribe', old_hub, old_topic)
if hub and topic: if hub and topic:
current_app.logger.debug('subscribing hub=%s, topic=%s', hub, topic)
send_request('subscribe', hub, topic) send_request('subscribe', hub, topic)
db.session.commit() 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 """Render the new entries and publish them to redis
""" """
from flask import render_template from flask import render_template
import flask.ext.login as flask_login import flask.ext.login as flask_login
current_app.logger.debug( current_app.logger.debug('notifying feed updated: %s', feed_id)
'notifying feed updated for entries %r', entry_ids)
feed = Feed.query.get(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: for s in feed.subscriptions:
with app.test_request_context(): with app.test_request_context():
flask_login.login_user(s.user, remember=True) flask_login.login_user(s.user, remember=True)
@ -336,16 +343,18 @@ def is_content_equal(e1, e2):
content = COMMENT_RE.sub('', content) content = COMMENT_RE.sub('', content)
return content return content
return (e1.title == e2.title return (
and normalize(e1.content) == normalize(e2.content) e1.title == e2.title
and e1.author_name == e2.author_name and normalize(e1.content) == normalize(e2.content)
and e1.author_url == e2.author_url and e1.author_name == e2.author_name
and e1.author_photo == e2.author_photo and e1.author_url == e2.author_url
and e1.properties == e2.properties) and e1.author_photo == e2.author_photo
and e1.properties == e2.properties
)
def process_xml_feed_for_new_entries(feed, content, backfill, now): 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={ parsed = feedparser.parse(content, response_headers={
'content-location': feed.feed, '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_name = feed_props.get('author_detail', {}).get('name')
default_author_photo = feed_props.get('logo') 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) # work from the bottom up (oldest first, usually)
for p_entry in reversed(parsed.entries): for p_entry in reversed(parsed.entries):
current_app.logger.debug('processing entry {}'.format( current_app.logger.debug('processing entry %s', str(p_entry)[:32])
str(p_entry)[:256]))
permalink = p_entry.get('link') permalink = p_entry.get('link')
uid = p_entry.get('id') or permalink 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')) video = VIDEO_ENCLOSURE_TMPL.format(href=link.get('href'))
content = (content or '') + video content = (content or '') + video
current_app.logger.debug('building entry')
entry = Entry( entry = Entry(
published=published, published=published,
updated=updated, updated=updated,
@ -422,6 +432,8 @@ def process_xml_feed_for_new_entries(feed, content, backfill, now):
author_photo=default_author_photo author_photo=default_author_photo
or fallback_photo(feed.origin)) or fallback_photo(feed.origin))
current_app.logger.debug('yielding entry')
yield entry yield entry
@ -462,7 +474,7 @@ def hentry_to_entry(hentry, feed, backfill, now):
title = hentry.get('name') title = hentry.get('name')
content = hentry.get('content') content = hentry.get('content')
if not content: if not content and hentry.get('type') == 'entry':
content = title content = title
title = None title = None
@ -518,6 +530,16 @@ def hentry_to_entry(hentry, feed, backfill, now):
if value: if value:
entry.set_property(prop, 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? # does it look like a jam?
plain = hentry.get('content-plain') plain = hentry.get('content-plain')
if plain and JAM_RE.match(plain): if plain and JAM_RE.match(plain):
@ -527,25 +549,28 @@ def hentry_to_entry(hentry, feed, backfill, now):
return entry return entry
def fetch_reply_context(entry_id, in_reply_to, now): def fetch_reply_context(entry, in_reply_to, now):
with flask_app(): context = Entry.query\
entry = Entry.query.get(entry_id) .join(Entry.feed)\
context = Entry.query\ .filter(Entry.permalink==in_reply_to, Feed.type == 'html')\
.join(Entry.feed)\ .first()
.filter(Entry.permalink==in_reply_to, Feed.type == 'html')\
.first()
if not context: if not context:
current_app.logger.info('fetching in-reply-to url: %s', current_app.logger.info('fetching in-reply-to: %s', in_reply_to)
in_reply_to) try:
proxied_reply_url = proxy_url(in_reply_to)
parsed = mf2util.interpret( 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: if parsed:
context = hentry_to_entry(parsed, None, False, now) 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: if context:
entry.reply_context.append(context) db.session.add(context)
db.session.commit() entry.reply_context.append(context)
def proxy_url(url): def proxy_url(url):

View file

@ -46,6 +46,18 @@
<h1>{{ entry.title }}</h1> <h1>{{ entry.title }}</h1>
{% endif %} {% endif %}
{% if entry.get_property('event') %}
<p>
{% if entry.get_property('start') %}
<strong>start:</strong> {{ entry.get_property('start') }}
{% endif %}
<br/>
{% if entry.get_property('end') %}
<strong>end:</strong> {{ entry.get_property('end') }}
{% endif %}
</p>
{% endif %}
{% set photo = entry.get_property('photo') %} {% set photo = entry.get_property('photo') %}
{% if photo and (not entry.content or '<img' not in entry.content) %} {% if photo and (not entry.content or '<img' not in entry.content) %}
<div class="photo"> <div class="photo">

View file

@ -4,11 +4,20 @@
{% if reply_method == 'micropub' and current_user.micropub_endpoint %} {% if reply_method == 'micropub' and current_user.micropub_endpoint %}
<form class="micropub-form" action="{{ url_for('api.publish') }}" method="POST"> <form class="micropub-form" action="{{ url_for('api.publish') }}" method="POST">
<input type="hidden" name="target" value="{{ entry.permalink }}"/> <input type="hidden" name="target" value="{{ entry.permalink }}"/>
{% if entry.get_property('event') %}
<div class="rsvps">
<button type="submit" name="action" value="rsvp-yes">Going</button>
<button type="submit" name="action" value="rsvp-maybe">Interested</button>
<button type="submit" name="action" value="rsvp-no">Not Going</button>
</div>
{% endif %}
<div> <div>
<textarea class="content" name="content"></textarea> <textarea class="content" name="content"></textarea>
<button type="submit" name="action" value="reply"><i class="fa fa-reply"></i></button> <button type="submit" name="action" value="reply" class="small"><i class="fa fa-reply"></i></button>
<button type="submit" name="action" value="repost"><i class="fa fa-retweet"></i></button> <button type="submit" name="action" value="repost" class="small"><i class="fa fa-retweet"></i></button>
<button type="submit" name="action" value="like"><i class="fa fa-star"></i></button> <button type="submit" name="action" value="like" class="small"><i class="fa fa-star"></i></button>
</div> </div>
<div class="syndication-toggles"> <div class="syndication-toggles">
{% for target in current_user.get_setting('syndicate-to', []) %} {% for target in current_user.get_setting('syndicate-to', []) %}

View file

@ -8,14 +8,12 @@
<link rel="shortcut icon" href="{{ url_for('static', filename='logo.png') }}"/> <link rel="shortcut icon" href="{{ url_for('static', filename='logo.png') }}"/>
<link rel="apple-touch-icon" href="{{ url_for('static', filename='logo.png') }}"/> <link rel="apple-touch-icon" href="{{ url_for('static', filename='logo.png') }}"/>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css', version='2016-01-31') }}"/> <link rel="stylesheet" href="{{ url_for('static', filename='style.css', version='2016-03-08') }}"/>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"/> <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"/>
<script src="//code.jquery.com/jquery-2.1.3.min.js"></script> <script src="//code.jquery.com/jquery-2.1.3.min.js"></script>
<script src="{{ url_for('static', filename='moment.min.js') }}"></script> <script src="{{ url_for('static', filename='moment.min.js') }}"></script>
<script src="{{ url_for('static', filename='cassis.js') }}"></script> <script src="{{ url_for('static', filename='cassis.js') }}"></script>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
{% block head %}{% endblock %} {% block head %}{% endblock %}
</head> </head>
<body> <body>
@ -73,6 +71,10 @@
</main> </main>
{% block foot %}{% endblock %} {% block foot %}{% endblock %}
<div class="footer">
Woodwind is <a href="https://github.com/kylewm/woodwind">on GitHub</a>. Have any problems? File an issue or come chat in <code>#indiewebcamp</code> on Freenode IRC.
</div>
<script> <script>
$("input[type='url']").blur(function() { $("input[type='url']").blur(function() {
if (this.value.trim() != '') { if (this.value.trim() != '') {

View file

@ -4,7 +4,7 @@
{% if ws_topic %} {% if ws_topic %}
<script>var WS_TOPIC = "{{ ws_topic }}";</script> <script>var WS_TOPIC = "{{ ws_topic }}";</script>
{% endif %} {% endif %}
<script src="{{url_for('static', filename='feed.js', version='2015-12-24')}}"></script> <script src="{{url_for('static', filename='feed.js', version='2016-03-08')}}"></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' %}

View file

@ -12,6 +12,9 @@
{% if current_user.micropub_endpoint or current_user.access_token %} {% if current_user.micropub_endpoint or current_user.access_token %}
<input type="text" value="{{ current_user.micropub_endpoint }}" readonly /> <input type="text" value="{{ current_user.micropub_endpoint }}" readonly />
<input type="text" value="{{ current_user.access_token }}" readonly /> <input type="text" value="{{ current_user.access_token }}" readonly />
<p>
<a href="{{url_for('.micropub_update', next=request.path)}}">Update Syndication Targets</a>
</p>
<p> <p>
<a href="{{url_for('.authorize', next=request.path)}}">Reauthorize Micropub</a> <a href="{{url_for('.authorize', next=request.path)}}">Reauthorize Micropub</a>
</p> </p>

View file

@ -304,8 +304,39 @@ def micropub_callback(resp):
return flask.redirect(resp.next_url or flask.url_for('.index')) return flask.redirect(resp.next_url or flask.url_for('.index'))
@flask_login.login_required
@views.route('/micropub-update')
def micropub_update():
update_micropub_syndicate_to()
syndicate_to = flask_login.current_user.get_setting('syndicate-to', [])
flask.flash('Updated syndication targets: {}'.format(', '.join([
t.get('name') if isinstance(t, dict) else t for t in syndicate_to])))
return flask.redirect(flask.request.args.get('next')
or flask.url_for('.index'))
@flask_login.login_required @flask_login.login_required
def update_micropub_syndicate_to(): def update_micropub_syndicate_to():
def adapt_expanded(targets):
"""Backcompat support for old-style "syndicate-to-expanded" properties,
e.g.,
{
"id": "twitter::kylewmahan",
"name": "@kylewmahan",
"service": "Twitter"
}
"""
if targets:
return [{
'uid': t.get('id'),
'name': '{} on {}'.format(t.get('name'), t.get('service')),
} for t in targets]
return targets
endpt = flask_login.current_user.micropub_endpoint endpt = flask_login.current_user.micropub_endpoint
token = flask_login.current_user.access_token token = flask_login.current_user.access_token
if not endpt or not token: if not endpt or not token:
@ -331,9 +362,10 @@ def update_micropub_syndicate_to():
if content_type == 'application/json': if content_type == 'application/json':
blob = resp.json() blob = resp.json()
syndicate_tos = blob.get('syndicate-to-expanded') syndicate_tos = adapt_expanded(blob.get('syndicate-to-expanded'))
if not syndicate_tos: if not syndicate_tos:
syndicate_tos = blob.get('syndicate-to') syndicate_tos = blob.get('syndicate-to')
else: # try to parse query string else: # try to parse query string
syndicate_tos = pyquerystring.parse(resp.text).get('syndicate-to', []) syndicate_tos = pyquerystring.parse(resp.text).get('syndicate-to', [])
if isinstance(syndicate_tos, list): if isinstance(syndicate_tos, list):
@ -735,17 +767,15 @@ def font_awesome_class_for_service(service):
@views.app_template_filter('syndication_target_id') @views.app_template_filter('syndication_target_id')
def syndication_target_id(target): def syndication_target_id(target):
if isinstance(target, dict): if isinstance(target, dict):
return target.get('id') return target.get('uid') or target.get('id')
return target return target
@views.app_template_filter('render_syndication_target') @views.app_template_filter('render_syndication_target')
def render_syndication_target(target): def render_syndication_target(target):
if isinstance(target, dict): if isinstance(target, dict):
service = target.get('service') full_name = target.get('name')
name = target.get('name') return full_name
return '<i class="{}"></i>&nbsp;{}'.format(
font_awesome_class_for_service(service), name)
return '<img src="{}" alt="{}" />&nbsp;{}'.format( return '<img src="{}" alt="{}" />&nbsp;{}'.format(
favicon_for_url(target), target, prettify_url(target)) favicon_for_url(target), target, prettify_url(target))