mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 19:36:56 -08:00
federation - editing posts and sending replies to subscribers
This commit is contained in:
parent
4c58a04f8a
commit
01f3cf212b
9 changed files with 284 additions and 149 deletions
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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">
|
||||||
|
|
Loading…
Add table
Reference in a new issue