post soft-deletion: add options to restore or purge deleted posts

This commit is contained in:
freamon 2024-10-26 03:49:16 +00:00
parent 9a033522d1
commit 502e6ff0f6
4 changed files with 90 additions and 38 deletions

View file

@ -619,6 +619,7 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
post = create_post(activity_log, community, request_json, user) post = create_post(activity_log, community, request_json, user)
if post: if post:
announce_activity_to_followers(community, user, request_json) announce_activity_to_followers(community, user, request_json)
activity_log.result = 'success'
except TypeError as e: except TypeError as e:
activity_log.exception_message = 'TypeError. See log file.' activity_log.exception_message = 'TypeError. See log file.'
current_app.logger.error('TypeError: ' + str(request_json)) current_app.logger.error('TypeError: ' + str(request_json))
@ -1081,17 +1082,16 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
activity_log.result = 'success' activity_log.result = 'success'
elif request_json['object']['type'] == 'Delete': # undoing a delete elif request_json['object']['type'] == 'Delete': # undoing a delete
activity_log.activity_type = 'Restore' activity_log.activity_type = 'Restore'
reply = PostReply.query.filter_by(ap_id=request_json['object']['object']).first() post = Post.query.filter_by(ap_id=request_json['object']['object']).first()
if reply: if post:
deletor = find_actor_or_create(request_json['object']['actor'], create_if_not_found=False) deletor = find_actor_or_create(request_json['object']['actor'], create_if_not_found=False)
if deletor: if deletor:
if reply.author.id == deletor.id or reply.community.is_moderator(deletor) or reply.community.is_instance_admin(deletor): if post.author.id == deletor.id or post.community.is_moderator(deletor) or post.community.is_instance_admin(deletor):
reply.deleted = False post.deleted = False
reply.deleted_by = None post.deleted_by = None
if not reply.author.bot: post.author.post_count += 1
reply.post.reply_count += 1 post.community.post_count += 1
reply.author.post_reply_count += 1 announce_activity_to_followers(post.community, post.author, request_json)
announce_activity_to_followers(reply.community, reply.author, request_json)
db.session.commit() db.session.commit()
activity_log.result = 'success' activity_log.result = 'success'
else: else:
@ -1099,7 +1099,25 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
else: else:
activity_log.exception_message = 'Restorer did not already exist' activity_log.exception_message = 'Restorer did not already exist'
else: else:
activity_log.exception_message = 'Reply not found, or object was not a reply' reply = PostReply.query.filter_by(ap_id=request_json['object']['object']).first()
if reply:
deletor = find_actor_or_create(request_json['object']['actor'], create_if_not_found=False)
if deletor:
if reply.author.id == deletor.id or reply.community.is_moderator(deletor) or reply.community.is_instance_admin(deletor):
reply.deleted = False
reply.deleted_by = None
if not reply.author.bot:
reply.post.reply_count += 1
reply.author.post_reply_count += 1
announce_activity_to_followers(reply.community, reply.author, request_json)
db.session.commit()
activity_log.result = 'success'
else:
activity_log.exception_message = 'Restore attempt denied'
else:
activity_log.exception_message = 'Restorer did not already exist'
else:
activity_log.exception_message = 'Object not found, or object was not a post or a reply'
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

@ -1381,7 +1381,6 @@ def restore_post_or_comment_task(object_json, aplog_id):
if restorer and community and to_restore: if restorer and community and to_restore:
if to_restore.author.id == restorer.id or restorer.is_admin() or community.is_moderator(restorer) or community.is_instance_admin(restorer): if to_restore.author.id == restorer.id or restorer.is_admin() or community.is_moderator(restorer) or community.is_instance_admin(restorer):
if isinstance(to_restore, Post): if isinstance(to_restore, Post):
# TODO: restore_dependencies()
to_restore.deleted = False to_restore.deleted = False
to_restore.deleted_by = None to_restore.deleted_by = None
community.post_count += 1 community.post_count += 1

View file

@ -670,14 +670,18 @@ def add_reply(post_id: int, comment_id: int):
@bp.route('/post/<int:post_id>/options', methods=['GET']) @bp.route('/post/<int:post_id>/options', methods=['GET'])
def post_options(post_id: int): def post_options(post_id: int):
post = Post.query.get_or_404(post_id) post = Post.query.get_or_404(post_id)
if current_user.is_anonymous or not current_user.is_admin(): if post.deleted:
if post.deleted: if current_user.is_anonymous:
abort(404) abort(404)
if (not post.community.is_moderator() and
not current_user.is_admin() and
(post.deleted_by is not None and post.deleted_by != current_user.id)):
abort(401)
existing_bookmark = [] existing_bookmark = []
if current_user.is_authenticated: if current_user.is_authenticated:
existing_bookmark = PostBookmark.query.filter(PostBookmark.post_id == post_id, PostBookmark.user_id == current_user.id).first() existing_bookmark = PostBookmark.query.filter(PostBookmark.post_id == post_id, PostBookmark.user_id == current_user.id).first()
return render_template('post/post_options.html', post=post, existing_bookmark=existing_bookmark, return render_template('post/post_options.html', post=post, existing_bookmark=existing_bookmark,
moderating_communities=moderating_communities(current_user.get_id()), moderating_communities=moderating_communities(current_user.get_id()),
joined_communities=joined_communities(current_user.get_id()), joined_communities=joined_communities(current_user.get_id()),
@ -1092,14 +1096,19 @@ def post_delete_post(community: Community, post: Post, user_id: int, federate_al
@login_required @login_required
def post_restore(post_id: int): def post_restore(post_id: int):
post = Post.query.get_or_404(post_id) post = Post.query.get_or_404(post_id)
if post.community.is_moderator() or post.community.is_owner() or current_user.is_admin(): if post.user_id == current_user.id or post.community.is_moderator() or post.community.is_owner() or current_user.is_admin():
if post.deleted_by == post.user_id:
was_mod_deletion = False
else:
was_mod_deletion = True
post.deleted = False post.deleted = False
post.deleted_by = None
post.author.post_count += 1 post.author.post_count += 1
post.community.post_count += 1 post.community.post_count += 1
db.session.commit() db.session.commit()
# Federate un-delete # Federate un-delete
if post.is_local(): if not post.community.local_only:
delete_json = { delete_json = {
"actor": current_user.public_url(), "actor": current_user.public_url(),
"to": ["https://www.w3.org/ns/activitystreams#Public"], "to": ["https://www.w3.org/ns/activitystreams#Public"],
@ -1115,31 +1124,40 @@ def post_restore(post_id: int):
], ],
'object': post.ap_id, 'object': post.ap_id,
'uri': post.ap_id, 'uri': post.ap_id,
"summary": "bad post",
}, },
"cc": [post.community.public_url()], "cc": [post.community.public_url()],
"audience": post.author.public_url(), "audience": post.community.public_url(),
"type": "Undo", "type": "Undo",
"id": f"https://{current_app.config['SERVER_NAME']}/activities/undo/{gibberish(15)}" "id": f"https://{current_app.config['SERVER_NAME']}/activities/undo/{gibberish(15)}"
} }
if was_mod_deletion:
delete_json['object']['summary'] = "Deleted by mod"
announce = { if not post.community.is_local(): # this is a remote community, send it to the instance that hosts it
"id": f"https://{current_app.config['SERVER_NAME']}/activities/announce/{gibberish(15)}", if not was_mod_deletion or (was_mod_deletion and post.community.is_moderator(current_user)):
"type": 'Announce', success = post_request(post.community.ap_inbox_url, delete_json, current_user.private_key,
"to": [ current_user.public_url() + '#main-key')
"https://www.w3.org/ns/activitystreams#Public" if success is False or isinstance(success, str):
], flash('Failed to send delete to remote server', 'error')
"actor": post.community.public_url(),
"cc": [
post.community.ap_followers_url
],
'@context': default_context(),
'object': delete_json
}
for instance in post.community.following_instances(): else: # local community - send it to followers on remote instances
if instance.inbox and not current_user.has_blocked_instance(instance.id) and not instance_banned(instance.domain): announce = {
send_to_remote_instance(instance.id, post.community.id, 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.public_url(),
"cc": [
post.community.ap_followers_url
],
'@context': default_context(),
'object': delete_json
}
for instance in post.community.following_instances():
if instance.inbox and not current_user.has_blocked_instance(instance.id) and not instance_banned(instance.domain):
send_to_remote_instance(instance.id, post.community.id, announce)
if post.user_id != current_user.id: if post.user_id != current_user.id:
add_to_modlog('restore_post', community_id=post.community.id, link_text=shorten_string(post.title), add_to_modlog('restore_post', community_id=post.community.id, link_text=shorten_string(post.title),
@ -1149,6 +1167,23 @@ def post_restore(post_id: int):
return redirect(url_for('activitypub.post_ap', post_id=post.id)) return redirect(url_for('activitypub.post_ap', post_id=post.id))
@bp.route('/post/<int:post_id>/purge', methods=['GET', 'POST'])
@login_required
def post_purge(post_id: int):
post = Post.query.get_or_404(post_id)
if not post.deleted:
abort(404)
if post.deleted_by == current_user.id or post.community.is_moderator() or current_user.is_admin():
post.delete_dependencies()
db.session.delete(post)
db.session.commit()
flash(_('Post purged.'))
else:
abort(401)
return redirect(url_for('user.show_profile_by_id', user_id=post.user_id))
@bp.route('/post/<int:post_id>/bookmark', methods=['GET', 'POST']) @bp.route('/post/<int:post_id>/bookmark', methods=['GET', 'POST'])
@login_required @login_required
def post_bookmark(post_id: int): def post_bookmark(post_id: int):
@ -1749,9 +1784,7 @@ def post_reply_purge(post_id: int, comment_id: int):
post_reply = PostReply.query.get_or_404(comment_id) post_reply = PostReply.query.get_or_404(comment_id)
if not post_reply.deleted: if not post_reply.deleted:
abort(404) abort(404)
if post_reply.user_id == current_user.id and (post_reply.deleted_by is None or post_reply.deleted_by != post_reply.user_id): if post_reply.deleted_by == current_user.id or post.community.is_moderator() or current_user.is_admin():
abort(401)
if post_reply.user_id == current_user.id or post.community.is_moderator() or current_user.is_admin():
if not post_reply.has_replies(): if not post_reply.has_replies():
post_reply.delete_dependencies() post_reply.delete_dependencies()
db.session.delete(post_reply) db.session.delete(post_reply)

View file

@ -19,8 +19,10 @@
{% endif -%} {% endif -%}
{% if post.user_id == current_user.id or post.community.is_moderator() or post.community.is_owner() or current_user.is_admin() -%} {% if post.user_id == current_user.id or post.community.is_moderator() or post.community.is_owner() or current_user.is_admin() -%}
{% if post.deleted -%} {% if post.deleted -%}
<li><a href="{{ url_for('post.post_restore', post_id=post.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-delete"></span> <li><a href="{{ url_for('post.post_restore', post_id=post.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-arrow-up"></span>
{{ _('Restore') }}</a></li> {{ _('Restore') }}</a></li>
<li><a href="{{ url_for('post.post_purge', post_id=post.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-delete red"></span>
{{ _('Purge') }}</a></li>
{% else -%} {% else -%}
<li><a href="{{ url_for('post.post_delete', post_id=post.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-delete"></span> <li><a href="{{ url_for('post.post_delete', post_id=post.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-delete"></span>
{{ _('Delete') }}</a></li> {{ _('Delete') }}</a></li>