federation - editing posts and sending replies to subscribers

This commit is contained in:
rimu 2023-12-26 21:39:52 +13:00
parent 4c58a04f8a
commit 01f3cf212b
9 changed files with 284 additions and 149 deletions

View file

@ -453,7 +453,7 @@ def process_inbox_request(request_json, activitypublog_id):
activity_log.exception_message = 'Activity about local content which is already present' activity_log.exception_message = 'Activity about local content which is already present'
activity_log.result = 'ignored' activity_log.result = 'ignored'
# Announce is new content and votes, lemmy and mastodon style # Announce is new content and votes that happened on a remote server.
if request_json['type'] == 'Announce': if request_json['type'] == 'Announce':
if request_json['object']['type'] == 'Create': if request_json['object']['type'] == 'Create':
activity_log.activity_type = request_json['object']['type'] activity_log.activity_type = request_json['object']['type']
@ -511,6 +511,7 @@ def process_inbox_request(request_json, activitypublog_id):
if post is not None: if post is not None:
db.session.add(post) db.session.add(post)
community.post_count += 1 community.post_count += 1
activity_log.result = 'success'
db.session.commit() db.session.commit()
if post.image_id: if post.image_id:
make_image_sizes(post.image_id, 266, None, 'posts') make_image_sizes(post.image_id, 266, None, 'posts')
@ -623,6 +624,39 @@ def process_inbox_request(request_json, activitypublog_id):
to_be_deleted_ap_id = request_json['object']['object'] to_be_deleted_ap_id = request_json['object']['object']
delete_post_or_comment(user_ap_id, community_ap_id, to_be_deleted_ap_id) delete_post_or_comment(user_ap_id, community_ap_id, to_be_deleted_ap_id)
activity_log.result = 'success' activity_log.result = 'success'
elif request_json['object']['type'] == 'Page': # Editing a post
post = Post.query.filter_by(ap_id=request_json['object']['id']).first()
if post:
post.title = request_json['object']['name']
if 'source' in request_json['object'] and request_json['object']['source']['mediaType'] == 'text/markdown':
post.body = request_json['object']['source']['content']
post.body_html = markdown_to_html(post.body)
elif 'content' in request_json['object']:
post.body_html = allowlist_html(request_json['object']['content'])
post.body = html_to_markdown(post.body_html)
if 'attachment' in request_json['object'] and 'href' in request_json['object']['attachment']:
post.url = request_json['object']['attachment']['href']
post.edited_at = utcnow()
db.session.commit()
activity_log.result = 'success'
else:
activity_log.exception_message = 'Post not found'
elif request_json['object']['type'] == 'Note': # Editing a reply
reply = PostReply.query.filter_by(ap_id=request_json['object']['id']).first()
if reply:
if 'source' in request_json['object'] and request_json['object']['source']['mediaType'] == 'text/markdown':
reply.body = request_json['object']['source']['content']
reply.body_html = markdown_to_html(reply.body)
elif 'content' in request_json['object']:
reply.body_html = allowlist_html(request_json['object']['content'])
reply.body = html_to_markdown(reply.body_html)
reply.edited_at = utcnow()
db.session.commit()
activity_log.result = 'success'
else:
activity_log.exception_message = 'PostReply not found'
else:
activity_log.exception_message = 'Invalid type for Announce'
# Follow: remote user wants to join/follow one of our communities # Follow: remote user wants to join/follow one of our communities
elif request_json['type'] == 'Follow': # Follow is when someone wants to join a community elif request_json['type'] == 'Follow': # Follow is when someone wants to join a community
@ -781,6 +815,7 @@ def process_inbox_request(request_json, activitypublog_id):
activity_log.result = 'ignored' activity_log.result = 'ignored'
elif request_json['type'] == 'Update': elif request_json['type'] == 'Update':
activity_log.activity_type = 'Update'
if request_json['object']['type'] == 'Page': # Editing a post if request_json['object']['type'] == 'Page': # Editing a post
post = Post.query.filter_by(ap_id=request_json['object']['id']).first() post = Post.query.filter_by(ap_id=request_json['object']['id']).first()
if post: if post:
@ -790,9 +825,13 @@ def process_inbox_request(request_json, activitypublog_id):
elif 'content' in request_json['object']: elif 'content' in request_json['object']:
post.body_html = allowlist_html(request_json['object']['content']) post.body_html = allowlist_html(request_json['object']['content'])
post.body = html_to_markdown(post.body_html) post.body = html_to_markdown(post.body_html)
if 'attachment' in request_json['object'] and 'href' in request_json['object']['attachment']:
post.url = request_json['object']['attachment']['href']
post.edited_at = utcnow() post.edited_at = utcnow()
db.session.commit() db.session.commit()
activity_log.result = 'success' activity_log.result = 'success'
else:
activity_log.exception_message = 'Post not found'
elif request_json['object']['type'] == 'Note': # Editing a reply elif request_json['object']['type'] == 'Note': # Editing a reply
reply = PostReply.query.filter_by(ap_id=request_json['object']['id']).first() reply = PostReply.query.filter_by(ap_id=request_json['object']['id']).first()
if reply: if reply:
@ -805,6 +844,8 @@ def process_inbox_request(request_json, activitypublog_id):
reply.edited_at = utcnow() reply.edited_at = utcnow()
db.session.commit() db.session.commit()
activity_log.result = 'success' activity_log.result = 'success'
else:
activity_log.exception_message = 'PostReply not found'
elif request_json['type'] == 'Delete': elif request_json['type'] == 'Delete':
if isinstance(request_json['object'], str): if isinstance(request_json['object'], str):
ap_id = request_json['object'] # lemmy ap_id = request_json['object'] # lemmy

View file

@ -1,4 +1,4 @@
from flask import redirect, url_for, flash, request, make_response, session, Markup, current_app, abort from flask import redirect, url_for, flash, request, make_response, session, Markup, current_app, abort, g
from flask_login import login_user, logout_user, current_user, login_required from flask_login import login_user, logout_user, current_user, login_required
from flask_babel import _ from flask_babel import _
from sqlalchemy import or_, desc from sqlalchemy import or_, desc
@ -9,7 +9,7 @@ from app.activitypub.util import default_context
from app.community.forms import SearchRemoteCommunity, AddLocalCommunity, CreatePostForm, ReportCommunityForm, \ from app.community.forms import SearchRemoteCommunity, AddLocalCommunity, CreatePostForm, ReportCommunityForm, \
DeleteCommunityForm DeleteCommunityForm
from app.community.util import search_for_community, community_url_exists, actor_to_community, \ from app.community.util import search_for_community, community_url_exists, actor_to_community, \
opengraph_parse, url_to_thumbnail_file, save_post, save_icon_file, save_banner_file opengraph_parse, url_to_thumbnail_file, save_post, save_icon_file, save_banner_file, send_to_remote_instance
from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE, \ from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE, \
SUBSCRIPTION_PENDING SUBSCRIPTION_PENDING
from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post, \ from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post, \
@ -303,59 +303,83 @@ def add_post(actor):
form.communities.choices = [(c.id, c.display_name()) for c in current_user.communities()] form.communities.choices = [(c.id, c.display_name()) for c in current_user.communities()]
if form.validate_on_submit(): if form.validate_on_submit():
community = Community.query.get_or_404(form.communities.data)
post = Post(user_id=current_user.id, community_id=form.communities.data, instance_id=1) post = Post(user_id=current_user.id, community_id=form.communities.data, instance_id=1)
save_post(form, post) save_post(form, post)
community.post_count += 1 community.post_count += 1
community.last_active = utcnow() community.last_active = g.site.last_active = utcnow()
db.session.commit() db.session.commit()
post.ap_id = f"https://{current_app.config['SERVER_NAME']}/post/{post.id}" post.ap_id = f"https://{current_app.config['SERVER_NAME']}/post/{post.id}"
db.session.commit() db.session.commit()
page = {
'type': 'Page',
'id': post.ap_id,
'attributedTo': current_user.ap_profile_id,
'to': [
community.ap_profile_id,
'https://www.w3.org/ns/activitystreams#Public'
],
'name': post.title,
'cc': [],
'content': post.body_html,
'mediaType': 'text/html',
'source': {
'content': post.body,
'mediaType': 'text/markdown'
},
'attachment': [],
'commentsEnabled': post.comments_enabled,
'sensitive': post.nsfw,
'nsfl': post.nsfl,
'published': ap_datetime(utcnow()),
'audience': community.ap_profile_id
}
create = {
"id": f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}",
"actor": current_user.ap_profile_id,
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
community.ap_profile_id
],
"type": "Create",
"audience": community.ap_profile_id,
"object": page,
'@context': default_context()
}
if not community.is_local(): # this is a remote community - send the post to the instance that hosts it if not community.is_local(): # this is a remote community - send the post to the instance that hosts it
page = {
'type': 'Page',
'id': post.ap_id,
'attributedTo': current_user.ap_profile_id,
'to': [
community.ap_profile_id,
'https://www.w3.org/ns/activitystreams#Public'
],
'name': post.title,
'cc': [],
'content': post.body_html,
'mediaType': 'text/html',
'source': {
'content': post.body,
'mediaType': 'text/markdown'
},
'attachment': [],
'commentsEnabled': post.comments_enabled,
'sensitive': post.nsfw,
'nsfl': post.nsfl,
'published': ap_datetime(utcnow()),
'audience': community.ap_profile_id
}
create = {
"id": f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}",
"actor": current_user.ap_profile_id,
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
community.ap_profile_id
],
"type": "Create",
"audience": community.ap_profile_id,
"object": page
}
success = post_request(community.ap_inbox_url, create, current_user.private_key, success = post_request(community.ap_inbox_url, create, current_user.private_key,
current_user.ap_profile_id + '#main-key') current_user.ap_profile_id + '#main-key')
if success: if success:
flash('Your post has been sent to ' + community.title) flash(_('Your post to %(name)s has been made.', name=community.title))
else: else:
flash('There was a problem sending your post to ' + community.title) flash('There was a problem making your post to ' + community.title)
else: # local community - send post out to followers else: # local community - send post out to followers
... announce = {
"id": f"https://{current_app.config['SERVER_NAME']}/activities/announce/{gibberish(15)}",
"type": 'Announce',
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"actor": community.ap_profile_id,
"cc": [
community.ap_followers_url
],
'@context': default_context(),
'object': create
}
sent_to = 0
for instance in community.following_instances():
if instance[1] and not current_user.has_blocked_instance(instance[0]):
send_to_remote_instance(instance[1], community.id, announce)
sent_to += 1
if sent_to:
flash(_('Your post to %(name)s has been made.', name=community.title))
else:
flash(_('Your post to %(name)s has been made.', name=community.title))
return redirect(f"/c/{community.link()}") return redirect(f"/c/{community.link()}")
else: else:
@ -366,8 +390,8 @@ def add_post(actor):
images_disabled=images_disabled, markdown_editor=True) images_disabled=images_disabled, markdown_editor=True)
@login_required
@bp.route('/community/<int:community_id>/report', methods=['GET', 'POST']) @bp.route('/community/<int:community_id>/report', methods=['GET', 'POST'])
@login_required
def community_report(community_id: int): def community_report(community_id: int):
community = Community.query.get_or_404(community_id) community = Community.query.get_or_404(community_id)
form = ReportCommunityForm() form = ReportCommunityForm()
@ -396,8 +420,8 @@ def community_report(community_id: int):
return render_template('community/community_report.html', title=_('Report community'), form=form, community=community) return render_template('community/community_report.html', title=_('Report community'), form=form, community=community)
@login_required
@bp.route('/community/<int:community_id>/delete', methods=['GET', 'POST']) @bp.route('/community/<int:community_id>/delete', methods=['GET', 'POST'])
@login_required
def community_delete(community_id: int): def community_delete(community_id: int):
community = Community.query.get_or_404(community_id) community = Community.query.get_or_404(community_id)
if community.is_owner() or current_user.is_admin(): if community.is_owner() or current_user.is_admin():
@ -415,8 +439,8 @@ def community_delete(community_id: int):
abort(401) abort(401)
@login_required
@bp.route('/community/<int:community_id>/block_instance', methods=['GET', 'POST']) @bp.route('/community/<int:community_id>/block_instance', methods=['GET', 'POST'])
@login_required
def community_block_instance(community_id: int): def community_block_instance(community_id: int):
community = Community.query.get_or_404(community_id) community = Community.query.get_or_404(community_id)
existing = InstanceBlock.query.filter_by(user_id=current_user.id, instance_id=community.instance_id).first() existing = InstanceBlock.query.filter_by(user_id=current_user.id, instance_id=community.instance_id).first()

View file

@ -9,6 +9,7 @@ from flask_login import current_user
from pillow_heif import register_heif_opener from pillow_heif import register_heif_opener
from app import db, cache, celery from app import db, cache, celery
from app.activitypub.signature import post_request
from app.activitypub.util import find_actor_or_create, actor_json_to_model, post_json_to_model from app.activitypub.util import find_actor_or_create, actor_json_to_model, post_json_to_model
from app.constants import POST_TYPE_ARTICLE, POST_TYPE_LINK, POST_TYPE_IMAGE from app.constants import POST_TYPE_ARTICLE, POST_TYPE_LINK, POST_TYPE_IMAGE
from app.models import Community, File, BannedInstances, PostReply, PostVote, Post, utcnow, CommunityMember, Site from app.models import Community, File, BannedInstances, PostReply, PostVote, Post, utcnow, CommunityMember, Site
@ -351,3 +352,17 @@ def save_banner_file(banner_file, directory='communities') -> File:
width=img_width, height=img_height, thumbnail_width=thumbnail_width, thumbnail_height=thumbnail_height) width=img_width, height=img_height, thumbnail_width=thumbnail_width, thumbnail_height=thumbnail_height)
db.session.add(file) db.session.add(file)
return file return file
def send_to_remote_instance(inbox, community_id, payload):
if current_app.debug:
send_to_remote_instance_task(inbox, community_id, payload)
else:
send_to_remote_instance_task.delay(inbox, community_id, payload)
@celery.task
def send_to_remote_instance_task(inbox, community_id, payload):
community = Community.query.get(community_id)
if community:
post_request(inbox, payload, community.private_key, community.ap_profile_id + '#main-key')

View file

@ -201,6 +201,12 @@ class Community(db.Model):
else: else:
return f"https://{current_app.config['SERVER_NAME']}/c/{self.ap_id}" return f"https://{current_app.config['SERVER_NAME']}/c/{self.ap_id}"
# returns a list of tuples (instance.id, instance.inbox)
def following_instances(self):
sql = 'select distinct i.id, i.inbox from "instance" as i inner join "user" as u on u.instance_id = i.id inner join "community_member" as cm on cm.user_id = u.id '
sql += 'where cm.community_id = :community_id and cm.is_banned = false'
return db.session.execute(text(sql), {'community_id': self.id})
def delete_dependencies(self): def delete_dependencies(self):
# this will be fine for remote communities but for local ones it is necessary to federate every deletion out to subscribers # this will be fine for remote communities but for local ones it is necessary to federate every deletion out to subscribers
for post in self.posts: for post in self.posts:
@ -410,6 +416,10 @@ class User(UserMixin, db.Model):
def created_recently(self): def created_recently(self):
return self.created and self.created > utcnow() - timedelta(days=7) return self.created and self.created > utcnow() - timedelta(days=7)
def has_blocked_instance(self, instance_id):
instance_block = InstanceBlock.query.filter_by(user_id=self.id, instance_id=instance_id).first()
return instance_block is not None
@staticmethod @staticmethod
def verify_reset_password_token(token): def verify_reset_password_token(token):
try: try:

View file

@ -8,7 +8,7 @@ from sqlalchemy import or_, desc
from app import db, constants from app import db, constants
from app.activitypub.signature import HttpSignature, post_request from app.activitypub.signature import HttpSignature, post_request
from app.activitypub.util import default_context from app.activitypub.util import default_context
from app.community.util import save_post from app.community.util import save_post, send_to_remote_instance
from app.post.forms import NewReplyForm, ReportPostForm, MeaCulpaForm from app.post.forms import NewReplyForm, ReportPostForm, MeaCulpaForm
from app.community.forms import CreatePostForm from app.community.forms import CreatePostForm
from app.post.util import post_replies, get_comment_branch, post_reply_count from app.post.util import post_replies, get_comment_branch, post_reply_count
@ -68,48 +68,63 @@ def show_post(post_id: int):
post.flush_cache() post.flush_cache()
# federation # federation
reply_json = {
'type': 'Note',
'id': reply.profile_id(),
'attributedTo': current_user.profile_id(),
'to': [
'https://www.w3.org/ns/activitystreams#Public'
],
'cc': [
post.community.profile_id(),
],
'content': reply.body_html,
'inReplyTo': post.profile_id(),
'mediaType': 'text/html',
'source': {
'content': reply.body,
'mediaType': 'text/markdown'
},
'published': ap_datetime(utcnow()),
'distinguished': False,
'audience': post.community.profile_id()
}
create_json = {
'type': 'Create',
'actor': current_user.profile_id(),
'audience': post.community.profile_id(),
'to': [
'https://www.w3.org/ns/activitystreams#Public'
],
'cc': [
post.community.ap_profile_id
],
'object': reply_json,
'id': f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}"
}
if not post.community.is_local(): # this is a remote community, send it to the instance that hosts it if not post.community.is_local(): # this is a remote community, send it to the instance that hosts it
reply_json = {
'type': 'Note',
'id': reply.profile_id(),
'attributedTo': current_user.profile_id(),
'to': [
'https://www.w3.org/ns/activitystreams#Public'
],
'cc': [
post.community.profile_id(),
],
'content': reply.body_html,
'inReplyTo': post.profile_id(),
'mediaType': 'text/html',
'source': {
'content': reply.body,
'mediaType': 'text/markdown'
},
'published': ap_datetime(utcnow()),
'distinguished': False,
'audience': post.community.profile_id()
}
create_json = {
'type': 'Create',
'actor': current_user.profile_id(),
'audience': post.community.profile_id(),
'to': [
'https://www.w3.org/ns/activitystreams#Public'
],
'cc': [
post.community.ap_profile_id
],
'object': reply_json,
'id': f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}"
}
success = post_request(post.community.ap_inbox_url, create_json, current_user.private_key, success = post_request(post.community.ap_inbox_url, create_json, current_user.private_key,
current_user.ap_profile_id + '#main-key') current_user.ap_profile_id + '#main-key')
if not success: if not success:
flash('Failed to send to remote instance', 'error') flash('Failed to send to remote instance', 'error')
else: # local community - send it to followers on remote instances else: # local community - send it to followers on remote instances
... announce = {
"id": f"https://{current_app.config['SERVER_NAME']}/activities/announce/{gibberish(15)}",
"type": 'Announce',
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"actor": post.community.ap_profile_id,
"cc": [
post.community.ap_followers_url
],
'@context': default_context(),
'object': create_json
}
for instance in post.community.following_instances():
if instance[1] and not current_user.has_blocked_instance(instance[0]):
send_to_remote_instance(instance[1], post.community.id, announce)
return redirect(url_for('activitypub.post_ap', return redirect(url_for('activitypub.post_ap',
post_id=post_id)) # redirect to current page to avoid refresh resubmitting the form post_id=post_id)) # redirect to current page to avoid refresh resubmitting the form
@ -319,64 +334,81 @@ def add_reply(post_id: int, comment_id: int):
post.flush_cache() post.flush_cache()
# federation # federation
if not post.community.is_local(): # this is a remote community, send it to the instance that hosts it reply_json = {
reply_json = { 'type': 'Note',
'type': 'Note', 'id': reply.profile_id(),
'id': reply.profile_id(), 'attributedTo': current_user.profile_id(),
'attributedTo': current_user.profile_id(), 'to': [
'to': [ 'https://www.w3.org/ns/activitystreams#Public',
'https://www.w3.org/ns/activitystreams#Public', in_reply_to.author.profile_id()
in_reply_to.author.profile_id() ],
], 'cc': [
'cc': [ post.community.profile_id(),
post.community.profile_id(), current_user.followers_url()
current_user.followers_url() ],
], 'content': reply.body_html,
'content': reply.body_html, 'inReplyTo': in_reply_to.profile_id(),
'inReplyTo': in_reply_to.profile_id(), 'url': reply.profile_id(),
'url': reply.profile_id(), 'mediaType': 'text/html',
'mediaType': 'text/html', 'source': {
'source': { 'content': reply.body,
'content': reply.body, 'mediaType': 'text/markdown'
'mediaType': 'text/markdown' },
}, 'published': ap_datetime(utcnow()),
'published': ap_datetime(utcnow()), 'distinguished': False,
'distinguished': False, 'audience': post.community.profile_id(),
'audience': post.community.profile_id(), 'contentMap': {
'contentMap': { 'en': reply.body_html
'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',
in_reply_to.author.profile_id()
],
'cc': [
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:
reply_json['tag'] = [
{
'href': in_reply_to.author.ap_profile_id,
'name': '@' + in_reply_to.author.ap_id,
'type': 'Mention'
} }
} ]
create_json = { if not post.community.is_local(): # this is a remote community, send it to the instance that hosts it
'@context': default_context(),
'type': 'Create',
'actor': current_user.profile_id(),
'audience': post.community.profile_id(),
'to': [
'https://www.w3.org/ns/activitystreams#Public',
in_reply_to.author.profile_id()
],
'cc': [
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:
reply_json['tag'] = [
{
'href': in_reply_to.author.ap_profile_id,
'name': '@' + in_reply_to.author.ap_id,
'type': 'Mention'
}
]
success = post_request(post.community.ap_inbox_url, create_json, current_user.private_key, success = post_request(post.community.ap_inbox_url, create_json, current_user.private_key,
current_user.ap_profile_id + '#main-key') current_user.ap_profile_id + '#main-key')
if not success: if not success:
flash('Failed to send reply', 'error') flash('Failed to send reply', 'error')
else: # local community - send it to followers on remote instances else: # local community - send it to followers on remote instances
... announce = {
"id": f"https://{current_app.config['SERVER_NAME']}/activities/announce/{gibberish(15)}",
"type": 'Announce',
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"actor": post.community.ap_profile_id,
"cc": [
post.community.ap_followers_url
],
'@context': default_context(),
'object': create_json
}
for instance in post.community.following_instances():
if instance[1] and not current_user.has_blocked_instance(instance[0]):
send_to_remote_instance(instance[1], post.community.id, announce)
if reply.depth <= constants.THREAD_CUTOFF_DEPTH: if reply.depth <= constants.THREAD_CUTOFF_DEPTH:
return redirect(url_for('activitypub.post_ap', 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: else:
@ -392,8 +424,9 @@ def post_options(post_id: int):
post = Post.query.get_or_404(post_id) post = Post.query.get_or_404(post_id)
return render_template('post/post_options.html', post=post) return render_template('post/post_options.html', post=post)
@login_required
@bp.route('/post/<int:post_id>/edit', methods=['GET', 'POST']) @bp.route('/post/<int:post_id>/edit', methods=['GET', 'POST'])
@login_required
def post_edit(post_id: int): def post_edit(post_id: int):
post = Post.query.get_or_404(post_id) post = Post.query.get_or_404(post_id)
form = CreatePostForm() form = CreatePostForm()
@ -427,13 +460,14 @@ def post_edit(post_id: int):
form.type.data = 'image' form.type.data = 'image'
form.image_title.data = post.title form.image_title.data = post.title
form.notify_author.data = post.notify_author form.notify_author.data = post.notify_author
return render_template('post/post_edit.html', title=_('Edit post'), form=form, post=post, images_disabled=images_disabled) return render_template('post/post_edit.html', title=_('Edit post'), form=form, post=post,
images_disabled=images_disabled, markdown_editor=True)
else: else:
abort(401) abort(401)
@login_required
@bp.route('/post/<int:post_id>/delete', methods=['GET', 'POST']) @bp.route('/post/<int:post_id>/delete', methods=['GET', 'POST'])
@login_required
def post_delete(post_id: int): def post_delete(post_id: int):
post = Post.query.get_or_404(post_id) post = Post.query.get_or_404(post_id)
community = post.community community = post.community
@ -447,8 +481,8 @@ def post_delete(post_id: int):
return redirect(url_for('activitypub.community_profile', actor=community.ap_id if community.ap_id is not None else community.name)) return redirect(url_for('activitypub.community_profile', actor=community.ap_id if community.ap_id is not None else community.name))
@login_required
@bp.route('/post/<int:post_id>/report', methods=['GET', 'POST']) @bp.route('/post/<int:post_id>/report', methods=['GET', 'POST'])
@login_required
def post_report(post_id: int): def post_report(post_id: int):
post = Post.query.get_or_404(post_id) post = Post.query.get_or_404(post_id)
form = ReportPostForm() form = ReportPostForm()
@ -479,8 +513,8 @@ def post_report(post_id: int):
return render_template('post/post_report.html', title=_('Report post'), form=form, post=post) return render_template('post/post_report.html', title=_('Report post'), form=form, post=post)
@login_required
@bp.route('/post/<int:post_id>/block_user', methods=['GET', 'POST']) @bp.route('/post/<int:post_id>/block_user', methods=['GET', 'POST'])
@login_required
def post_block_user(post_id: int): def post_block_user(post_id: int):
post = Post.query.get_or_404(post_id) post = Post.query.get_or_404(post_id)
existing = UserBlock.query.filter_by(blocker_id=current_user.id, blocked_id=post.author.id).first() existing = UserBlock.query.filter_by(blocker_id=current_user.id, blocked_id=post.author.id).first()
@ -494,8 +528,8 @@ def post_block_user(post_id: int):
return redirect(post.community.local_url()) return redirect(post.community.local_url())
@login_required
@bp.route('/post/<int:post_id>/block_domain', methods=['GET', 'POST']) @bp.route('/post/<int:post_id>/block_domain', methods=['GET', 'POST'])
@login_required
def post_block_domain(post_id: int): def post_block_domain(post_id: int):
post = Post.query.get_or_404(post_id) post = Post.query.get_or_404(post_id)
existing = DomainBlock.query.filter_by(user_id=current_user.id, domain_id=post.domain_id).first() existing = DomainBlock.query.filter_by(user_id=current_user.id, domain_id=post.domain_id).first()
@ -506,8 +540,8 @@ def post_block_domain(post_id: int):
return redirect(post.community.local_url()) return redirect(post.community.local_url())
@login_required
@bp.route('/post/<int:post_id>/block_instance', methods=['GET', 'POST']) @bp.route('/post/<int:post_id>/block_instance', methods=['GET', 'POST'])
@login_required
def post_block_instance(post_id: int): def post_block_instance(post_id: int):
post = Post.query.get_or_404(post_id) post = Post.query.get_or_404(post_id)
existing = InstanceBlock.query.filter_by(user_id=current_user.id, instance_id=post.instance_id).first() existing = InstanceBlock.query.filter_by(user_id=current_user.id, instance_id=post.instance_id).first()
@ -518,8 +552,8 @@ def post_block_instance(post_id: int):
return redirect(post.community.local_url()) return redirect(post.community.local_url())
@login_required
@bp.route('/post/<int:post_id>/mea_culpa', methods=['GET', 'POST']) @bp.route('/post/<int:post_id>/mea_culpa', methods=['GET', 'POST'])
@login_required
def post_mea_culpa(post_id: int): def post_mea_culpa(post_id: int):
post = Post.query.get_or_404(post_id) post = Post.query.get_or_404(post_id)
form = MeaCulpaForm() form = MeaCulpaForm()

View file

@ -1,5 +1,3 @@
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,600;0,700;0,800;1,300;1,400;1,600;1,700;1,800&display=swap');
.downarea, .downarea * { .downarea, .downarea * {
margin: 0; margin: 0;
padding: 0; padding: 0;
@ -7,7 +5,6 @@
-webkit-box-sizing: border-box; -webkit-box-sizing: border-box;
-moz-box-sizing: border-box; -moz-box-sizing: border-box;
box-sizing: border-box; box-sizing: border-box;
font-family: 'Open Sans', -apple-system, 'Segoe UI', sans-serif;
} }
.downarea::-webkit-scrollbar , .downarea *::-webkit-scrollbar { .downarea::-webkit-scrollbar , .downarea *::-webkit-scrollbar {
@ -195,8 +192,6 @@
padding: 8px; padding: 8px;
margin-bottom: 20px; margin-bottom: 20px;
resize: none; resize: none;
font-size: 14px;
font-weight: 500;
} }
.downarea .downarea-bottom { .downarea .downarea-bottom {

View file

@ -532,6 +532,10 @@ nav.navbar {
text-decoration: none; text-decoration: none;
} }
.alert {
width: 96%;
}
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
body { body {
background-color: #777; background-color: #777;

View file

@ -239,6 +239,11 @@ nav.navbar {
width: 41px; width: 41px;
text-decoration: none; text-decoration: none;
} }
.alert {
width: 96%;
}
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
body { body {
background-color: $dark-grey; background-color: $dark-grey;

View file

@ -24,6 +24,13 @@
if(toClick) { if(toClick) {
toClick.click(); toClick.click();
} }
var downarea = new DownArea({
elem: document.querySelector('#discussion_body'),
resize: DownArea.RESIZE_VERTICAL,
hide: ['heading', 'bold-italic'],
value: {{ form.discussion_body.data | tojson | safe }}
});
}); });
</script> </script>
<div class="row"> <div class="row">