diff --git a/app/activitypub/routes.py b/app/activitypub/routes.py index c182d132..0656f9f5 100644 --- a/app/activitypub/routes.py +++ b/app/activitypub/routes.py @@ -199,7 +199,7 @@ def community_profile(actor): if '@' in actor: # don't provide activitypub info for remote communities if 'application/ld+json' in request.headers.get('Accept', '') or 'application/activity+json' in request.headers.get('Accept', ''): - abort(404) + abort(400) community: Community = Community.query.filter_by(ap_id=actor, banned=False).first() else: community: Community = Community.query.filter_by(name=actor, banned=False, ap_id=None).first() diff --git a/app/community/routes.py b/app/community/routes.py index 7ea18c36..e8cb894e 100644 --- a/app/community/routes.py +++ b/app/community/routes.py @@ -19,8 +19,8 @@ from app.utils import get_setting, render_template, allowlist_html, markdown_to_ shorten_string, markdown_to_text, domain_from_url, validate_image, gibberish, community_membership, ap_datetime, \ request_etag_matches, return_304 import os -from PIL import Image, ImageOps -from datetime import datetime +from feedgen.feed import FeedGenerator +from datetime import timezone @bp.route('/add_local', methods=['GET', 'POST']) @@ -117,7 +117,59 @@ def show_community(community: Community): return render_template('community/community.html', community=community, title=community.title, is_moderator=is_moderator, is_owner=is_owner, mods=mod_list, posts=posts, description=description, og_image=og_image, POST_TYPE_IMAGE=POST_TYPE_IMAGE, POST_TYPE_LINK=POST_TYPE_LINK, SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING, - SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER, etag=f"{community.id}_{hash(community.last_active)}") + SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER, etag=f"{community.id}_{hash(community.last_active)}", + rss_feed=f"https://{current_app.config['SERVER_NAME']}/community/{community.link()}/feed", rss_feed_name=f"{community.title} posts on PieFed") + + +# RSS feed of the community +@bp.route('//feed', methods=['GET']) +@cache.cached(timeout=600) +def show_community_rss(actor): + actor = actor.strip() + if '@' in actor: + community: Community = Community.query.filter_by(ap_id=actor, banned=False).first() + else: + community: Community = Community.query.filter_by(name=actor, banned=False, ap_id=None).first() + if community is not None: + # If nothing has changed since their last visit, return HTTP 304 + current_etag = f"{community.id}_{hash(community.last_active)}" + if request_etag_matches(current_etag): + return return_304(current_etag, 'application/rss+xml') + + posts = community.posts.filter(Post.from_bot == False).order_by(desc(Post.created_at)).limit(100).all() + description = shorten_string(community.description, 150) if community.description else None + og_image = community.image.source_url if community.image_id else None + fg = FeedGenerator() + fg.id(f"https://{current_app.config['SERVER_NAME']}/c/{actor}") + fg.title(community.title) + fg.link(href=f"https://{current_app.config['SERVER_NAME']}/c/{actor}", rel='alternate') + if og_image: + fg.logo(og_image) + else: + fg.logo(f"https://{current_app.config['SERVER_NAME']}/static/images/apple-touch-icon.png") + if description: + fg.subtitle(description) + else: + fg.subtitle(' ') + fg.link(href=f"https://{current_app.config['SERVER_NAME']}/c/{actor}/feed", rel='self') + fg.language('en') + + for post in posts: + fe = fg.add_entry() + fe.title(post.title) + fe.link(href=f"https://{current_app.config['SERVER_NAME']}/post/{post.id}") + fe.description(post.body_html) + fe.guid(post.profile_id(), permalink=True) + fe.author(name=post.author.user_name) + fe.pubDate(post.created_at.replace(tzinfo=timezone.utc)) + + response = make_response(fg.rss_str()) + response.headers.set('Content-Type', 'application/rss+xml') + response.headers.add_header('ETag', f"{community.id}_{hash(community.last_active)}") + response.headers.add_header('Cache-Control', 'no-cache, max-age=600, must-revalidate') + return response + else: + abort(404) @bp.route('//subscribe', methods=['GET']) diff --git a/app/static/scss/_typography.scss b/app/static/scss/_typography.scss index b9a49587..359201c0 100644 --- a/app/static/scss/_typography.scss +++ b/app/static/scss/_typography.scss @@ -186,6 +186,10 @@ content: "\e91e"; } +.fe-rss::before { + content: "\e9be"; +} + .fe-image { position: relative; top: 2px; diff --git a/app/static/structure.css b/app/static/structure.css index 250d6aa0..dace866b 100644 --- a/app/static/structure.css +++ b/app/static/structure.css @@ -189,6 +189,10 @@ nav, etc which are used site-wide */ content: "\e91e"; } +.fe-rss::before { + content: "\e9be"; +} + .fe-image { position: relative; top: 2px; diff --git a/app/static/styles.css b/app/static/styles.css index 4e10378b..9355f73b 100644 --- a/app/static/styles.css +++ b/app/static/styles.css @@ -188,6 +188,10 @@ content: "\e91e"; } +.fe-rss::before { + content: "\e9be"; +} + .fe-image { position: relative; top: 2px; diff --git a/app/templates/base.html b/app/templates/base.html index f2941a8f..91923518 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -48,6 +48,9 @@ {% if og_image %} {% endif %} + {% if rss_feed %} + + {% endif %} {% endblock %} diff --git a/app/templates/community/community.html b/app/templates/community/community.html index d3670d73..8fcd9844 100644 --- a/app/templates/community/community.html +++ b/app/templates/community/community.html @@ -81,6 +81,9 @@ {% endfor %} {% endif %} +

+ RSS feed +

{% if is_moderator %} diff --git a/app/utils.py b/app/utils.py index 742262c2..7bb08058 100644 --- a/app/utils.py +++ b/app/utils.py @@ -37,16 +37,19 @@ def render_template(template_name: str, **context) -> Response: def request_etag_matches(etag): + print(str(request.headers)) if 'If-None-Match' in request.headers: old_etag = request.headers['If-None-Match'] return old_etag == etag return False -def return_304(etag): +def return_304(etag, content_type=None): resp = make_response('', 304) resp.headers.add_header('ETag', request.headers['If-None-Match']) resp.headers.add_header('Cache-Control', 'no-cache, max-age=600, must-revalidate') + if content_type: + resp.headers.set('Content-Type', content_type) return resp diff --git a/requirements.txt b/requirements.txt index 19f533b7..473ad2e2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,3 +25,4 @@ flask-caching==2.0.2 Pillow pillow-heif opengraph-parse=0.0.6 +feedgen==0.9.0