mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 19:36:56 -08:00
rss feeds on communities
This commit is contained in:
parent
5b6c9f39b2
commit
ac8a229475
9 changed files with 79 additions and 5 deletions
|
@ -199,7 +199,7 @@ def community_profile(actor):
|
||||||
if '@' in actor:
|
if '@' in actor:
|
||||||
# don't provide activitypub info for remote communities
|
# 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', ''):
|
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()
|
community: Community = Community.query.filter_by(ap_id=actor, banned=False).first()
|
||||||
else:
|
else:
|
||||||
community: Community = Community.query.filter_by(name=actor, banned=False, ap_id=None).first()
|
community: Community = Community.query.filter_by(name=actor, banned=False, ap_id=None).first()
|
||||||
|
|
|
@ -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, \
|
shorten_string, markdown_to_text, domain_from_url, validate_image, gibberish, community_membership, ap_datetime, \
|
||||||
request_etag_matches, return_304
|
request_etag_matches, return_304
|
||||||
import os
|
import os
|
||||||
from PIL import Image, ImageOps
|
from feedgen.feed import FeedGenerator
|
||||||
from datetime import datetime
|
from datetime import timezone
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/add_local', methods=['GET', 'POST'])
|
@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,
|
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,
|
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,
|
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'])
|
@bp.route('/<actor>/subscribe', methods=['GET'])
|
||||||
|
|
|
@ -186,6 +186,10 @@
|
||||||
content: "\e91e";
|
content: "\e91e";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fe-rss::before {
|
||||||
|
content: "\e9be";
|
||||||
|
}
|
||||||
|
|
||||||
.fe-image {
|
.fe-image {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
|
|
|
@ -189,6 +189,10 @@ nav, etc which are used site-wide */
|
||||||
content: "\e91e";
|
content: "\e91e";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fe-rss::before {
|
||||||
|
content: "\e9be";
|
||||||
|
}
|
||||||
|
|
||||||
.fe-image {
|
.fe-image {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
|
|
|
@ -188,6 +188,10 @@
|
||||||
content: "\e91e";
|
content: "\e91e";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fe-rss::before {
|
||||||
|
content: "\e9be";
|
||||||
|
}
|
||||||
|
|
||||||
.fe-image {
|
.fe-image {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
|
|
|
@ -48,6 +48,9 @@
|
||||||
{% if og_image %}
|
{% if og_image %}
|
||||||
<meta property="og:image" content="{{ og_image }}" />
|
<meta property="og:image" content="{{ og_image }}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if rss_feed %}
|
||||||
|
<link rel="alternate" type="application/rss+xml" title="{{ rss_feed_name }}" href="{{ rss_feed }}" />
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body class="d-flex flex-column" style="padding-top: 78px;">
|
<body class="d-flex flex-column" style="padding-top: 78px;">
|
||||||
|
|
|
@ -81,6 +81,9 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ol>
|
</ol>
|
||||||
{% endif %}
|
{% 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>
|
||||||
</div>
|
</div>
|
||||||
{% if is_moderator %}
|
{% if is_moderator %}
|
||||||
|
|
|
@ -37,16 +37,19 @@ def render_template(template_name: str, **context) -> Response:
|
||||||
|
|
||||||
|
|
||||||
def request_etag_matches(etag):
|
def request_etag_matches(etag):
|
||||||
|
print(str(request.headers))
|
||||||
if 'If-None-Match' in request.headers:
|
if 'If-None-Match' in request.headers:
|
||||||
old_etag = request.headers['If-None-Match']
|
old_etag = request.headers['If-None-Match']
|
||||||
return old_etag == etag
|
return old_etag == etag
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def return_304(etag):
|
def return_304(etag, content_type=None):
|
||||||
resp = make_response('', 304)
|
resp = make_response('', 304)
|
||||||
resp.headers.add_header('ETag', request.headers['If-None-Match'])
|
resp.headers.add_header('ETag', request.headers['If-None-Match'])
|
||||||
resp.headers.add_header('Cache-Control', 'no-cache, max-age=600, must-revalidate')
|
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
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -25,3 +25,4 @@ flask-caching==2.0.2
|
||||||
Pillow
|
Pillow
|
||||||
pillow-heif
|
pillow-heif
|
||||||
opengraph-parse=0.0.6
|
opengraph-parse=0.0.6
|
||||||
|
feedgen==0.9.0
|
||||||
|
|
Loading…
Reference in a new issue