rss feeds on communities

This commit is contained in:
rimu 2023-12-12 18:28:49 +13:00
parent 5b6c9f39b2
commit ac8a229475
9 changed files with 79 additions and 5 deletions

View file

@ -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()

View file

@ -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('/<actor>/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('/<actor>/subscribe', methods=['GET'])

View file

@ -186,6 +186,10 @@
content: "\e91e";
}
.fe-rss::before {
content: "\e9be";
}
.fe-image {
position: relative;
top: 2px;

View file

@ -189,6 +189,10 @@ nav, etc which are used site-wide */
content: "\e91e";
}
.fe-rss::before {
content: "\e9be";
}
.fe-image {
position: relative;
top: 2px;

View file

@ -188,6 +188,10 @@
content: "\e91e";
}
.fe-rss::before {
content: "\e9be";
}
.fe-image {
position: relative;
top: 2px;

View file

@ -48,6 +48,9 @@
{% if og_image %}
<meta property="og:image" content="{{ og_image }}" />
{% endif %}
{% if rss_feed %}
<link rel="alternate" type="application/rss+xml" title="{{ rss_feed_name }}" href="{{ rss_feed }}" />
{% endif %}
{% endblock %}
</head>
<body class="d-flex flex-column" style="padding-top: 78px;">

View file

@ -81,6 +81,9 @@
{% endfor %}
</ol>
{% endif %}
<p class="mt-4">
<a class="no-underline" href="{{ rss_feed }}" rel="nofollow"><span class="fe fe-rss"></span> RSS feed</a>
</p>
</div>
</div>
{% if is_moderator %}

View file

@ -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

View file

@ -25,3 +25,4 @@ flask-caching==2.0.2
Pillow
pillow-heif
opengraph-parse=0.0.6
feedgen==0.9.0