mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 19:36:56 -08:00
federating replies and lots of caching
This commit is contained in:
parent
7fd8935983
commit
094708f396
11 changed files with 128 additions and 50 deletions
|
@ -13,7 +13,7 @@ from app.models import User, Community, CommunityJoinRequest, CommunityMember, C
|
|||
PostReply, Instance, PostVote, PostReplyVote, File, AllowedInstances, BannedInstances
|
||||
from app.activitypub.util import public_key, users_total, active_half_year, active_month, local_posts, local_comments, \
|
||||
post_to_activity, find_actor_or_create, default_context, instance_blocked, find_reply_parent, find_liked_object, \
|
||||
lemmy_site_data, instance_weight
|
||||
lemmy_site_data, instance_weight, cache_key_by_ap_header, is_activitypub_request
|
||||
from app.utils import gibberish, get_setting, is_image_url, allowlist_html, html_to_markdown, render_template, \
|
||||
domain_from_url, markdown_to_html, community_membership, ap_datetime
|
||||
import werkzeug.exceptions
|
||||
|
@ -69,6 +69,7 @@ def webfinger():
|
|||
|
||||
|
||||
@bp.route('/.well-known/nodeinfo')
|
||||
@cache.cached(timeout=600)
|
||||
def nodeinfo():
|
||||
nodeinfo_data = {"links": [{"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
||||
"href": f"https://{current_app.config['SERVER_NAME']}/nodeinfo/2.0"}]}
|
||||
|
@ -77,6 +78,7 @@ def nodeinfo():
|
|||
|
||||
@bp.route('/nodeinfo/2.0')
|
||||
@bp.route('/nodeinfo/2.0.json')
|
||||
@cache.cached(timeout=600)
|
||||
def nodeinfo2():
|
||||
|
||||
nodeinfo_data = {
|
||||
|
@ -103,11 +105,13 @@ def nodeinfo2():
|
|||
|
||||
|
||||
@bp.route('/api/v3/site')
|
||||
@cache.cached(timeout=600)
|
||||
def lemmy_site():
|
||||
return jsonify(lemmy_site_data())
|
||||
|
||||
|
||||
@bp.route('/api/v3/federated_instances')
|
||||
@cache.cached(timeout=600)
|
||||
def lemmy_federated_instances():
|
||||
instances = Instance.query.all()
|
||||
linked = []
|
||||
|
@ -133,11 +137,8 @@ def lemmy_federated_instances():
|
|||
})
|
||||
|
||||
|
||||
def is_activitypub_request():
|
||||
return 'application/ld+json' in request.headers.get('Accept', '') or 'application/activity+json' in request.headers.get('Accept', '')
|
||||
|
||||
|
||||
@bp.route('/u/<actor>', methods=['GET'])
|
||||
@cache.cached(timeout=10, make_cache_key=cache_key_by_ap_header)
|
||||
def user_profile(actor):
|
||||
""" Requests to this endpoint can be for a JSON representation of the user, or a HTML rendering of their profile.
|
||||
The two types of requests are differentiated by the header """
|
||||
|
@ -290,7 +291,8 @@ def shared_inbox():
|
|||
community = find_actor_or_create(community_ap_id)
|
||||
user = find_actor_or_create(user_ap_id)
|
||||
if user and community:
|
||||
user.last_seen = datetime.utcnow()
|
||||
user.last_seen = community.last_active = datetime.utcnow()
|
||||
|
||||
object_type = request_json['object']['type']
|
||||
new_content_types = ['Page', 'Article', 'Link', 'Note']
|
||||
if object_type in new_content_types: # create a new post
|
||||
|
@ -399,7 +401,7 @@ def shared_inbox():
|
|||
community = find_actor_or_create(community_ap_id)
|
||||
user = find_actor_or_create(user_ap_id)
|
||||
if user and community:
|
||||
user.last_seen = datetime.utcnow()
|
||||
user.last_seen = community.last_active = datetime.utcnow()
|
||||
object_type = request_json['object']['object']['type']
|
||||
new_content_types = ['Page', 'Article', 'Link', 'Note']
|
||||
if object_type in new_content_types: # create a new post
|
||||
|
@ -853,6 +855,13 @@ def shared_inbox():
|
|||
activity_log.result = 'success'
|
||||
else:
|
||||
activity_log.exception_message = 'Could not find user or content for vote'
|
||||
# Flush the caches of any major object that was created. To be sure.
|
||||
if 'user' in vars() and user is not None:
|
||||
user.flush_cache()
|
||||
#if 'community' in vars() and community is not None:
|
||||
# community.flush_cache()
|
||||
if 'post' in vars() and post is not None:
|
||||
post.flush_cache()
|
||||
else:
|
||||
activity_log.exception_message = 'Instance banned'
|
||||
else:
|
||||
|
@ -889,6 +898,7 @@ def community_outbox(actor):
|
|||
|
||||
|
||||
@bp.route('/c/<actor>/moderators', methods=['GET'])
|
||||
@cache.cached(timeout=10, make_cache_key=cache_key_by_ap_header)
|
||||
def community_moderators(actor):
|
||||
actor = actor.strip()
|
||||
community = Community.query.filter_by(name=actor, banned=False, ap_id=None).first()
|
||||
|
@ -935,6 +945,7 @@ def inbox(actor):
|
|||
|
||||
|
||||
@bp.route('/comment/<int:comment_id>', methods=['GET'])
|
||||
@cache.cached(timeout=10, make_cache_key=cache_key_by_ap_header)
|
||||
def comment_ap(comment_id):
|
||||
if is_activitypub_request():
|
||||
reply = PostReply.query.get_or_404(comment_id)
|
||||
|
@ -974,6 +985,7 @@ def comment_ap(comment_id):
|
|||
|
||||
|
||||
@bp.route('/post/<int:post_id>', methods=['GET', 'POST'])
|
||||
@cache.cached(timeout=10, make_cache_key=cache_key_by_ap_header)
|
||||
def post_ap(post_id):
|
||||
if request.method == 'GET' and is_activitypub_request():
|
||||
post = Post.query.get_or_404(post_id)
|
||||
|
|
|
@ -2,7 +2,7 @@ import json
|
|||
import os
|
||||
from datetime import datetime
|
||||
from typing import Union, Tuple
|
||||
from flask import current_app
|
||||
from flask import current_app, request
|
||||
from sqlalchemy import text
|
||||
from app import db, cache
|
||||
from app.models import User, Post, Community, BannedInstances, File, PostReply, AllowedInstances, Instance
|
||||
|
@ -405,6 +405,15 @@ def instance_weight(domain):
|
|||
return 1.0
|
||||
|
||||
|
||||
def is_activitypub_request():
|
||||
return 'application/ld+json' in request.headers.get('Accept', '') or 'application/activity+json' in request.headers.get('Accept', '')
|
||||
|
||||
|
||||
# differentiate between cached JSON and cached HTML by appending is_activitypub_request() to the cache key
|
||||
def cache_key_by_ap_header(**kwargs):
|
||||
return request.path + "_" + str(is_activitypub_request())
|
||||
|
||||
|
||||
def lemmy_site_data():
|
||||
data = {
|
||||
"site_view": {
|
||||
|
|
|
@ -16,7 +16,8 @@ from app.models import User, Community, CommunityMember, CommunityJoinRequest, C
|
|||
File, PostVote
|
||||
from app.community import bp
|
||||
from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \
|
||||
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
|
||||
import os
|
||||
from PIL import Image, ImageOps
|
||||
from datetime import datetime
|
||||
|
@ -87,6 +88,12 @@ def add_remote():
|
|||
|
||||
# @bp.route('/c/<actor>', methods=['GET']) - defined in activitypub/routes.py, which calls this function for user requests. A bit weird.
|
||||
def show_community(community: Community):
|
||||
|
||||
# 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)
|
||||
|
||||
mods = community.moderators()
|
||||
|
||||
is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods)
|
||||
|
@ -110,7 +117,7 @@ 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)
|
||||
SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER, etag=f"{community.id}_{hash(community.last_active)}")
|
||||
|
||||
|
||||
@bp.route('/<actor>/subscribe', methods=['GET'])
|
||||
|
|
|
@ -355,8 +355,6 @@ class User(UserMixin, db.Model):
|
|||
def profile_id(self):
|
||||
return self.ap_profile_id if self.ap_profile_id else f"https://{current_app.config['SERVER_NAME']}/u/{self.user_name}"
|
||||
|
||||
|
||||
|
||||
def created_recently(self):
|
||||
return self.created and self.created > datetime.utcnow() - timedelta(days=7)
|
||||
|
||||
|
@ -369,6 +367,10 @@ class User(UserMixin, db.Model):
|
|||
return
|
||||
return User.query.get(id)
|
||||
|
||||
def flush_cache(self):
|
||||
cache.delete('/u/' + self.user_name + '_False')
|
||||
cache.delete('/u/' + self.user_name + '_True')
|
||||
|
||||
def purge_content(self):
|
||||
files = File.query.join(Post).filter(Post.user_id == self.id).all()
|
||||
for file in files:
|
||||
|
@ -446,6 +448,7 @@ class Post(db.Model):
|
|||
image = db.relationship(File, lazy='joined', foreign_keys=[image_id], cascade="all, delete")
|
||||
domain = db.relationship('Domain', lazy='joined', foreign_keys=[domain_id])
|
||||
author = db.relationship('User', lazy='joined', overlaps='posts', foreign_keys=[user_id])
|
||||
replies = db.relationship('PostReply', lazy='dynamic', backref='post')
|
||||
|
||||
def is_local(self):
|
||||
return self.ap_id is None or self.ap_id.startswith('https://' + current_app.config['SERVER_NAME'])
|
||||
|
@ -472,6 +475,10 @@ class Post(db.Model):
|
|||
def profile_id(self):
|
||||
return f"https://{current_app.config['SERVER_NAME']}/post/{self.id}"
|
||||
|
||||
def flush_cache(self):
|
||||
cache.delete(f'/post/{self.id}_False')
|
||||
cache.delete(f'/post/{self.id}_True')
|
||||
|
||||
|
||||
class PostReply(db.Model):
|
||||
query_class = FullTextSearchQuery
|
||||
|
@ -515,7 +522,10 @@ class PostReply(db.Model):
|
|||
return cls.query.filter_by(ap_id=ap_id).first()
|
||||
|
||||
def profile_id(self):
|
||||
return f"https://{current_app.config['SERVER_NAME']}/comment/{self.id}"
|
||||
if self.ap_id:
|
||||
return self.ap_id
|
||||
else:
|
||||
return f"https://{current_app.config['SERVER_NAME']}/comment/{self.id}"
|
||||
|
||||
# the ap_id of the parent object, whether it's another PostReply or a Post
|
||||
def in_reply_to(self):
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
from datetime import datetime
|
||||
|
||||
from flask import redirect, url_for, flash, request, make_response, session, Markup, current_app, abort
|
||||
from flask import redirect, url_for, flash, current_app, abort
|
||||
from flask_login import login_user, logout_user, current_user, login_required
|
||||
from flask_babel import _
|
||||
from sqlalchemy import or_, desc
|
||||
|
||||
from app import db, constants
|
||||
from app.activitypub.signature import HttpSignature
|
||||
from app.activitypub.util import default_context
|
||||
from app.community.util import save_post
|
||||
from app.post.forms import NewReplyForm
|
||||
from app.community.forms import CreatePostForm
|
||||
|
@ -16,11 +17,18 @@ from app.models import Post, PostReply, \
|
|||
PostReplyVote, PostVote, Notification
|
||||
from app.post import bp
|
||||
from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \
|
||||
shorten_string, markdown_to_text, domain_from_url, validate_image, gibberish, ap_datetime
|
||||
shorten_string, markdown_to_text, domain_from_url, validate_image, gibberish, ap_datetime, return_304, \
|
||||
request_etag_matches
|
||||
|
||||
|
||||
def show_post(post_id: int):
|
||||
post = Post.query.get_or_404(post_id)
|
||||
|
||||
# If nothing has changed since their last visit, return HTTP 304
|
||||
current_etag = f"{post.id}_{hash(post.last_active)}"
|
||||
if request_etag_matches(current_etag):
|
||||
return return_304(current_etag)
|
||||
|
||||
mods = post.community.moderators()
|
||||
is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods)
|
||||
|
||||
|
@ -33,11 +41,12 @@ def show_post(post_id: int):
|
|||
notify_author=form.notify_author.data)
|
||||
if post.notify_author and current_user.id != post.user_id: # todo: check if replier is blocked
|
||||
notification = Notification(title=_('Reply: ') + shorten_string(form.body.data), user_id=post.user_id,
|
||||
author_id=current_user.id, url=url_for('post.show_post', post_id=post.id))
|
||||
author_id=current_user.id, url=url_for('activitypub.post_ap', post_id=post.id))
|
||||
db.session.add(notification)
|
||||
post.last_active = post.community.last_active = datetime.utcnow()
|
||||
post.reply_count += 1
|
||||
post.community.post_reply_count += 1
|
||||
|
||||
db.session.add(reply)
|
||||
db.session.commit()
|
||||
reply.ap_id = reply.profile_id()
|
||||
|
@ -47,7 +56,9 @@ def show_post(post_id: int):
|
|||
db.session.commit()
|
||||
form.body.data = ''
|
||||
flash('Your comment has been added.')
|
||||
# todo: flush cache
|
||||
|
||||
post.flush_cache()
|
||||
|
||||
# federation
|
||||
if not post.community.is_local(): # this is a remote community, send it to the instance that hosts it
|
||||
reply_json = {
|
||||
|
@ -100,7 +111,7 @@ def show_post(post_id: int):
|
|||
else: # local community - send it to followers on remote instances
|
||||
...
|
||||
|
||||
return redirect(url_for('post.show_post',
|
||||
return redirect(url_for('activitypub.post_ap',
|
||||
post_id=post_id)) # redirect to current page to avoid refresh resubmitting the form
|
||||
else:
|
||||
replies = post_replies(post.id, 'top')
|
||||
|
@ -112,7 +123,8 @@ def show_post(post_id: int):
|
|||
return render_template('post/post.html', title=post.title, post=post, is_moderator=is_moderator,
|
||||
canonical=post.ap_id, form=form, replies=replies, THREAD_CUTOFF_DEPTH=constants.THREAD_CUTOFF_DEPTH,
|
||||
description=description, og_image=og_image, POST_TYPE_IMAGE=constants.POST_TYPE_IMAGE,
|
||||
POST_TYPE_LINK=constants.POST_TYPE_LINK, POST_TYPE_ARTICLE=constants.POST_TYPE_ARTICLE)
|
||||
POST_TYPE_LINK=constants.POST_TYPE_LINK, POST_TYPE_ARTICLE=constants.POST_TYPE_ARTICLE,
|
||||
etag=f"{post.id}_{hash(post.last_active)}")
|
||||
|
||||
|
||||
@bp.route('/post/<int:post_id>/<vote_direction>', methods=['GET', 'POST'])
|
||||
|
@ -163,6 +175,7 @@ def post_vote(post_id: int, vote_direction):
|
|||
db.session.add(vote)
|
||||
current_user.last_seen = datetime.utcnow()
|
||||
db.session.commit()
|
||||
post.flush_cache()
|
||||
return render_template('post/_post_voting_buttons.html', post=post,
|
||||
upvoted_class=upvoted_class,
|
||||
downvoted_class=downvoted_class)
|
||||
|
@ -214,6 +227,7 @@ def comment_vote(comment_id, vote_direction):
|
|||
db.session.add(vote)
|
||||
current_user.last_seen = datetime.utcnow()
|
||||
db.session.commit()
|
||||
comment.post.flush_cache()
|
||||
return render_template('post/_voting_buttons.html', comment=comment,
|
||||
upvoted_class=upvoted_class,
|
||||
downvoted_class=downvoted_class)
|
||||
|
@ -249,7 +263,7 @@ def add_reply(post_id: int, comment_id: int):
|
|||
db.session.add(reply)
|
||||
if in_reply_to.notify_author and current_user.id != in_reply_to.user_id and in_reply_to.author.ap_id is None: # todo: check if replier is blocked
|
||||
notification = Notification(title=_('Reply: ') + shorten_string(form.body.data), user_id=in_reply_to.user_id,
|
||||
author_id=current_user.id, url=url_for('post.show_post', post_id=post.id))
|
||||
author_id=current_user.id, url=url_for('activitypub.post_ap', post_id=post.id))
|
||||
db.session.add(notification)
|
||||
db.session.commit()
|
||||
reply.ap_id = reply.profile_id()
|
||||
|
@ -262,7 +276,8 @@ def add_reply(post_id: int, comment_id: int):
|
|||
db.session.commit()
|
||||
form.body.data = ''
|
||||
flash('Your comment has been added.')
|
||||
# todo: flush cache
|
||||
|
||||
post.flush_cache()
|
||||
|
||||
# federation
|
||||
if not post.community.is_local(): # this is a remote community, send it to the instance that hosts it
|
||||
|
@ -271,13 +286,16 @@ def add_reply(post_id: int, comment_id: int):
|
|||
'id': reply.profile_id(),
|
||||
'attributedTo': current_user.profile_id(),
|
||||
'to': [
|
||||
'https://www.w3.org/ns/activitystreams#Public'
|
||||
'https://www.w3.org/ns/activitystreams#Public',
|
||||
in_reply_to.author.profile_id()
|
||||
],
|
||||
'cc': [
|
||||
post.community.profile_id(),
|
||||
current_user.followers_url()
|
||||
],
|
||||
'content': reply.body_html,
|
||||
'inReplyTo': in_reply_to.profile_id(),
|
||||
'url': reply.profile_id(),
|
||||
'mediaType': 'text/html',
|
||||
'source': {
|
||||
'content': reply.body,
|
||||
|
@ -285,31 +303,28 @@ def add_reply(post_id: int, comment_id: int):
|
|||
},
|
||||
'published': ap_datetime(datetime.utcnow()),
|
||||
'distinguished': False,
|
||||
'audience': post.community.profile_id()
|
||||
'audience': post.community.profile_id(),
|
||||
'contentMap': {
|
||||
'en': reply.body_html
|
||||
}
|
||||
}
|
||||
create_json = {
|
||||
'@context': default_context(),
|
||||
'type': 'Create',
|
||||
'actor': current_user.profile_id(),
|
||||
'audience': post.community.profile_id(),
|
||||
'to': [
|
||||
'https://www.w3.org/ns/activitystreams#Public'
|
||||
'https://www.w3.org/ns/activitystreams#Public',
|
||||
in_reply_to.author.profile_id()
|
||||
],
|
||||
'cc': [
|
||||
post.community.ap_profile_id
|
||||
post.community.profile_id(),
|
||||
current_user.followers_url()
|
||||
],
|
||||
'object': reply_json,
|
||||
'id': f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}"
|
||||
}
|
||||
if in_reply_to.notify_author and in_reply_to.author.ap_id is not None:
|
||||
create_json['cc'].append(in_reply_to.author.ap_profile_id)
|
||||
create_json['tag'] = [
|
||||
{
|
||||
'href': in_reply_to.author.ap_profile_id,
|
||||
'name': '@' + in_reply_to.author.ap_id,
|
||||
'type': 'Mention'
|
||||
}
|
||||
]
|
||||
reply_json['cc'].append(in_reply_to.author.ap_profile_id)
|
||||
reply_json['tag'] = [
|
||||
{
|
||||
'href': in_reply_to.author.ap_profile_id,
|
||||
|
@ -327,12 +342,12 @@ def add_reply(post_id: int, comment_id: int):
|
|||
current_app.logger.error('Response code for reply attempt was ' +
|
||||
str(message.status_code) + ' ' + message.text)
|
||||
except Exception as ex:
|
||||
flash('Failed to send request to subscribe: ' + str(ex), 'error')
|
||||
current_app.logger.error("Exception while trying to subscribe" + str(ex))
|
||||
flash('Failed to send reply: ' + str(ex), 'error')
|
||||
current_app.logger.error("Exception while trying to send reply" + str(ex))
|
||||
else: # local community - send it to followers on remote instances
|
||||
...
|
||||
if reply.depth <= constants.THREAD_CUTOFF_DEPTH:
|
||||
return redirect(url_for('post.show_post', post_id=post_id, _anchor=f'comment_{reply.parent_id}'))
|
||||
return redirect(url_for('activitypub.post_ap', post_id=post_id, _anchor=f'comment_{reply.parent_id}'))
|
||||
else:
|
||||
return redirect(url_for('post.continue_discussion', post_id=post_id, comment_id=reply.parent_id))
|
||||
else:
|
||||
|
@ -365,8 +380,9 @@ def post_edit(post_id: int):
|
|||
post.community.last_active = datetime.utcnow()
|
||||
post.edited_at = datetime.utcnow()
|
||||
db.session.commit()
|
||||
post.flush_cache()
|
||||
flash(_('Your changes have been saved.'), 'success')
|
||||
return redirect(url_for('post.show_post', post_id=post.id))
|
||||
return redirect(url_for('activitypub.post_ap', post_id=post.id))
|
||||
else:
|
||||
if post.type == constants.POST_TYPE_ARTICLE:
|
||||
form.type.data = 'discussion'
|
||||
|
@ -392,6 +408,7 @@ def post_delete(post_id: int):
|
|||
community = post.community
|
||||
if post.user_id == current_user.id or community.is_moderator():
|
||||
post.delete_dependencies()
|
||||
post.flush_cache()
|
||||
db.session.delete(post)
|
||||
db.session.commit()
|
||||
flash('Post deleted.')
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="row main_row">
|
||||
<div class="col">
|
||||
<h3>
|
||||
<a href="{{ url_for('post.show_post', post_id=post.id) }}">{{ post.title }}</a>
|
||||
<a href="{{ url_for('activitypub.post_ap', post_id=post.id) }}">{{ post.title }}</a>
|
||||
{% if post.type == POST_TYPE_IMAGE %}<span class="fe fe-image"> </span>{% endif %}
|
||||
{% if post.type == POST_TYPE_LINK and post.domain_id %}
|
||||
{% if post.url and 'youtube.com' in post.url %}
|
||||
|
@ -19,7 +19,7 @@
|
|||
<span class="small">{{ render_username(post.author) }} · {{ moment(post.posted_at).fromNow() }}</span>
|
||||
{% if post.image_id %}
|
||||
<div class="thumbnail">
|
||||
<a href="{{ url_for('post.show_post', post_id=post.id) }}"><img src="{{ post.image.thumbnail_url() }}" alt="{{ post.image.alt_text }}"
|
||||
<a href="{{ url_for('activitypub.post_ap', post_id=post.id) }}"><img src="{{ post.image.thumbnail_url() }}" alt="{{ post.image.alt_text }}"
|
||||
height="50" /></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -28,8 +28,8 @@
|
|||
</div>
|
||||
<div class="row utilities_row">
|
||||
<div class="col-6">
|
||||
<a href="{{ url_for('post.show_post', post_id=post.id, _anchor='replies') }}"><span class="fe fe-reply"></span></a>
|
||||
<a href="{{ url_for('post.show_post', post_id=post.id, _anchor='replies') }}">{{ post.reply_count }}</a>
|
||||
<a href="{{ url_for('activitypub.post_ap', post_id=post.id, _anchor='replies') }}"><span class="fe fe-reply"></span></a>
|
||||
<a href="{{ url_for('activitypub.post_ap', post_id=post.id, _anchor='replies') }}">{{ post.reply_count }}</a>
|
||||
</div>
|
||||
<div class="col-2"><a href="{{ url_for('post.post_options', post_id=post.id) }}"><span class="fe fe-options" title="Options"> </span></a></div>
|
||||
</div>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<div class="row">
|
||||
<div class="col-12 col-md-8 position-relative main_pane">
|
||||
{% include 'post/_post_full.html' %}
|
||||
<p><a href="{{ url_for('post.show_post', post_id=post.id, _anchor='replies') }}">Back to main discussion</a></p>
|
||||
<p><a href="{{ url_for('activitypub.post_ap', post_id=post.id, _anchor='replies') }}">Back to main discussion</a></p>
|
||||
<div class="row post_replies">
|
||||
<div class="col">
|
||||
{% macro render_comment(comment) %}
|
||||
|
|
|
@ -93,7 +93,7 @@
|
|||
<div class="card-body">
|
||||
<ul>
|
||||
{% for post in upvoted %}
|
||||
<li><a href="{{ url_for('post.show_post', post_id=post.id) }}">{{ post.title }}</a></li>
|
||||
<li><a href="{{ url_for('activitypub.post_ap', post_id=post.id) }}">{{ post.title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
|
|
|
@ -143,7 +143,7 @@
|
|||
|
||||
<ul>
|
||||
{% for post in upvoted %}
|
||||
<li><a href="{{ url_for('post.show_post', post_id=post.id) }}">{{ post.title }}</a></li>
|
||||
<li><a href="{{ url_for('activitypub.post_ap', post_id=post.id) }}">{{ post.title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ from flask import redirect, url_for, flash, request, make_response, session, Mar
|
|||
from flask_login import login_user, logout_user, current_user, login_required
|
||||
from flask_babel import _
|
||||
|
||||
from app import db
|
||||
from app import db, cache
|
||||
from app.models import Post, Community, CommunityMember, User, PostReply, PostVote, Notification
|
||||
from app.user import bp
|
||||
from app.user.forms import ProfileForm, SettingsForm
|
||||
|
@ -47,9 +47,11 @@ def edit_profile(actor):
|
|||
if form.password_field.data.strip() != '':
|
||||
current_user.set_password(form.password_field.data)
|
||||
current_user.about = form.about.data
|
||||
current_user.flush_cache()
|
||||
db.session.commit()
|
||||
|
||||
flash(_('Your changes have been saved.'), 'success')
|
||||
|
||||
return redirect(url_for('user.edit_profile', actor=actor))
|
||||
elif request.method == 'GET':
|
||||
form.email.data = current_user.email
|
||||
|
|
29
app/utils.py
29
app/utils.py
|
@ -11,7 +11,7 @@ from bs4 import BeautifulSoup
|
|||
import requests
|
||||
import os
|
||||
import imghdr
|
||||
from flask import current_app, json, redirect, url_for, request
|
||||
from flask import current_app, json, redirect, url_for, request, make_response, Response
|
||||
from flask_login import current_user
|
||||
from sqlalchemy import text
|
||||
|
||||
|
@ -20,12 +20,33 @@ from app.models import Settings, Domain, Instance, BannedInstances, User, Commun
|
|||
|
||||
|
||||
# Flask's render_template function, with support for themes added
|
||||
def render_template(template_name: str, **context) -> str:
|
||||
def render_template(template_name: str, **context) -> Response:
|
||||
theme = get_setting('theme', '')
|
||||
if theme != '':
|
||||
return flask.render_template(f'themes/{theme}/{template_name}', **context)
|
||||
content = flask.render_template(f'themes/{theme}/{template_name}', **context)
|
||||
else:
|
||||
return flask.render_template(template_name, **context)
|
||||
content = flask.render_template(template_name, **context)
|
||||
|
||||
# Browser caching using ETags and Cache-Control
|
||||
resp = make_response(content)
|
||||
if 'etag' in context:
|
||||
resp.headers.add_header('ETag', context['etag'])
|
||||
resp.headers.add_header('Cache-Control', 'no-cache, max-age=600, must-revalidate')
|
||||
return resp
|
||||
|
||||
|
||||
def request_etag_matches(etag):
|
||||
if 'If-None-Match' in request.headers:
|
||||
old_etag = request.headers['If-None-Match']
|
||||
return old_etag == etag
|
||||
return False
|
||||
|
||||
|
||||
def return_304(etag):
|
||||
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')
|
||||
return resp
|
||||
|
||||
|
||||
# Jinja: when a file was modified. Useful for cache-busting
|
||||
|
|
Loading…
Add table
Reference in a new issue