mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 11:26:56 -08:00
soft deletes and deleted content admin area #193
This commit is contained in:
parent
f4931b474c
commit
2380a3ae61
19 changed files with 326 additions and 107 deletions
|
@ -1010,7 +1010,7 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
|
|||
post.delete_dependencies()
|
||||
post.community.post_count -= 1
|
||||
announce_activity_to_followers(post.community, post.author, request_json)
|
||||
db.session.delete(post)
|
||||
post.deleted = True
|
||||
db.session.commit()
|
||||
activity_log.result = 'success'
|
||||
else:
|
||||
|
@ -1023,6 +1023,7 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
|
|||
reply.body_html = '<p><em>deleted</em></p>'
|
||||
reply.body = 'deleted'
|
||||
reply.post.reply_count -= 1
|
||||
reply.deleted = True
|
||||
announce_activity_to_followers(reply.community, reply.author, request_json)
|
||||
db.session.commit()
|
||||
activity_log.result = 'success'
|
||||
|
@ -1213,7 +1214,7 @@ def community_outbox(actor):
|
|||
actor = actor.strip()
|
||||
community = Community.query.filter_by(name=actor, banned=False, ap_id=None).first()
|
||||
if community is not None:
|
||||
posts = community.posts.limit(50).all()
|
||||
posts = community.posts.filter(Post.deleted == False).limit(50).all()
|
||||
|
||||
community_data = {
|
||||
"@context": default_context(),
|
||||
|
@ -1234,7 +1235,7 @@ def community_featured(actor):
|
|||
actor = actor.strip()
|
||||
community = Community.query.filter_by(name=actor, banned=False, ap_id=None).first()
|
||||
if community is not None:
|
||||
posts = Post.query.filter_by(community_id=community.id, sticky=True).all()
|
||||
posts = Post.query.filter_by(community_id=community.id, sticky=True, deleted=False).all()
|
||||
|
||||
community_data = {
|
||||
"@context": default_context(),
|
||||
|
|
|
@ -78,11 +78,11 @@ def active_day():
|
|||
|
||||
|
||||
def local_posts():
|
||||
return db.session.execute(text('SELECT COUNT(id) as c FROM "post" WHERE instance_id = 1')).scalar()
|
||||
return db.session.execute(text('SELECT COUNT(id) as c FROM "post" WHERE instance_id = 1 AND deleted is false')).scalar()
|
||||
|
||||
|
||||
def local_comments():
|
||||
return db.session.execute(text('SELECT COUNT(id) as c FROM "post_reply" WHERE instance_id = 1')).scalar()
|
||||
return db.session.execute(text('SELECT COUNT(id) as c FROM "post_reply" WHERE instance_id = 1 and deleted is false')).scalar()
|
||||
|
||||
|
||||
def local_communities():
|
||||
|
@ -1409,7 +1409,7 @@ def delete_post_or_comment_task(user_ap_id, community_ap_id, to_be_deleted_ap_id
|
|||
if deletor.is_admin() or community.is_moderator(deletor) or community.is_instance_admin(deletor) or to_delete.author.id == deletor.id:
|
||||
if isinstance(to_delete, Post):
|
||||
to_delete.delete_dependencies()
|
||||
db.session.delete(to_delete)
|
||||
to_delete.deleted = True
|
||||
community.post_count -= 1
|
||||
db.session.commit()
|
||||
elif isinstance(to_delete, PostReply):
|
||||
|
@ -1419,7 +1419,7 @@ def delete_post_or_comment_task(user_ap_id, community_ap_id, to_be_deleted_ap_id
|
|||
to_delete.body_html = lemmy_markdown_to_html(to_delete.body)
|
||||
else:
|
||||
to_delete.delete_dependencies()
|
||||
db.session.delete(to_delete)
|
||||
to_delete.deleted = True
|
||||
|
||||
db.session.commit()
|
||||
|
||||
|
@ -1447,31 +1447,30 @@ def remove_data_from_banned_user_task(deletor_ap_id, user_ap_id, target):
|
|||
|
||||
# community bans by mods
|
||||
elif community and community.is_moderator(deletor):
|
||||
post_replies = PostReply.query.filter_by(user_id=user.id, community_id=community.id)
|
||||
posts = Post.query.filter_by(user_id=user.id, community_id=community.id)
|
||||
|
||||
post_replies = PostReply.query.filter_by(user_id=user.id, community_id=community.id, deleted=False)
|
||||
posts = Post.query.filter_by(user_id=user.id, community_id=community.id, deleted=False)
|
||||
else:
|
||||
return
|
||||
|
||||
for pr in post_replies:
|
||||
pr.post.reply_count -= 1
|
||||
if pr.has_replies():
|
||||
pr.body = 'Banned'
|
||||
pr.body_html = lemmy_markdown_to_html(pr.body)
|
||||
for post_reply in post_replies:
|
||||
post_reply.post.reply_count -= 1
|
||||
if post_reply.has_replies():
|
||||
post_reply.body = 'Banned'
|
||||
post_reply.body_html = lemmy_markdown_to_html(post_reply.body)
|
||||
else:
|
||||
pr.delete_dependencies()
|
||||
db.session.delete(pr)
|
||||
post_reply.delete_dependencies()
|
||||
post_reply.deleted = True
|
||||
db.session.commit()
|
||||
|
||||
for p in posts:
|
||||
if p.cross_posts:
|
||||
old_cross_posts = Post.query.filter(Post.id.in_(p.cross_posts)).all()
|
||||
for post in posts:
|
||||
if post.cross_posts:
|
||||
old_cross_posts = Post.query.filter(Post.id.in_(post.cross_posts)).all()
|
||||
for ocp in old_cross_posts:
|
||||
if ocp.cross_posts is not None:
|
||||
ocp.cross_posts.remove(p.id)
|
||||
p.delete_dependencies()
|
||||
db.session.delete(p)
|
||||
p.community.post_count -= 1
|
||||
ocp.cross_posts.remove(post.id)
|
||||
post.delete_dependencies()
|
||||
post.deleted = True
|
||||
post.community.post_count -= 1
|
||||
db.session.commit()
|
||||
|
||||
|
||||
|
|
|
@ -552,7 +552,7 @@ def admin_content_trash():
|
|||
|
||||
page = request.args.get('page', 1, type=int)
|
||||
|
||||
posts = Post.query.filter(Post.posted_at > utcnow() - timedelta(days=3)).order_by(Post.score)
|
||||
posts = Post.query.filter(Post.posted_at > utcnow() - timedelta(days=3), Post.deleted == False).order_by(Post.score)
|
||||
posts = posts.paginate(page=page, per_page=100, error_out=False)
|
||||
|
||||
next_url = url_for('admin.admin_content_trash', page=posts.next_num) if posts.has_next else None
|
||||
|
@ -577,12 +577,14 @@ def admin_content_spam():
|
|||
posts = Post.query.join(User, User.id == Post.user_id).\
|
||||
filter(User.created > utcnow() - timedelta(days=3)).\
|
||||
filter(Post.posted_at > utcnow() - timedelta(days=3)).\
|
||||
filter(Post.deleted == False).\
|
||||
filter(Post.score <= 0).order_by(Post.score)
|
||||
posts = posts.paginate(page=page, per_page=100, error_out=False)
|
||||
|
||||
post_replies = PostReply.query.join(User, User.id == PostReply.user_id). \
|
||||
filter(User.created > utcnow() - timedelta(days=3)). \
|
||||
filter(PostReply.posted_at > utcnow() - timedelta(days=3)). \
|
||||
filter(PostReply.deleted == False). \
|
||||
filter(PostReply.score <= 0).order_by(PostReply.score)
|
||||
post_replies = post_replies.paginate(page=replies_page, per_page=100, error_out=False)
|
||||
|
||||
|
@ -602,6 +604,40 @@ def admin_content_spam():
|
|||
)
|
||||
|
||||
|
||||
@bp.route('/content/deleted', methods=['GET'])
|
||||
@login_required
|
||||
@permission_required('administer all users')
|
||||
def admin_content_deleted():
|
||||
# Shows all soft deleted posts
|
||||
page = request.args.get('page', 1, type=int)
|
||||
replies_page = request.args.get('replies_page', 1, type=int)
|
||||
|
||||
posts = Post.query.\
|
||||
filter(Post.deleted == True).\
|
||||
order_by(Post.posted_at)
|
||||
posts = posts.paginate(page=page, per_page=100, error_out=False)
|
||||
|
||||
post_replies = PostReply.query. \
|
||||
filter(PostReply.deleted == True). \
|
||||
order_by(PostReply.posted_at)
|
||||
post_replies = post_replies.paginate(page=replies_page, per_page=100, error_out=False)
|
||||
|
||||
next_url = url_for('admin.admin_content_deleted', page=posts.next_num) if posts.has_next else None
|
||||
prev_url = url_for('admin.admin_content_deleted', page=posts.prev_num) if posts.has_prev and page != 1 else None
|
||||
next_url_replies = url_for('admin.admin_content_deleted', replies_page=post_replies.next_num) if post_replies.has_next else None
|
||||
prev_url_replies = url_for('admin.admin_content_deleted', replies_page=post_replies.prev_num) if post_replies.has_prev and replies_page != 1 else None
|
||||
|
||||
return render_template('admin/deleted_posts.html', title=_('Deleted content'),
|
||||
next_url=next_url, prev_url=prev_url,
|
||||
next_url_replies=next_url_replies, prev_url_replies=prev_url_replies,
|
||||
posts=posts, post_replies=post_replies,
|
||||
moderating_communities=moderating_communities(current_user.get_id()),
|
||||
joined_communities=joined_communities(current_user.get_id()),
|
||||
menu_topics=menu_topics(),
|
||||
site=g.site
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/approve_registrations', methods=['GET'])
|
||||
@login_required
|
||||
@permission_required('approve registrations')
|
||||
|
|
|
@ -266,6 +266,15 @@ def register(app):
|
|||
InstanceRole.role == 'admin').delete()
|
||||
db.session.commit()
|
||||
|
||||
# Delete soft-deleted content after 7 days
|
||||
for post_reply in PostReply.query.filter(PostReply.deleted == True, PostReply.posted_at < utcnow() - timedelta(days=7)).all():
|
||||
db.session.delete(post_reply)
|
||||
|
||||
for post in Post.query.filter(Post.deleted == True, Post.posted_at < utcnow() - timedelta(days=7)).all():
|
||||
db.session.delete(post)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
@app.cli.command("spaceusage")
|
||||
def spaceusage():
|
||||
with app.app_context():
|
||||
|
|
|
@ -197,7 +197,7 @@ def show_community(community: Community):
|
|||
|
||||
# filter out nsfw and nsfl if desired
|
||||
if current_user.is_anonymous:
|
||||
posts = posts.filter(Post.from_bot == False, Post.nsfw == False, Post.nsfl == False)
|
||||
posts = posts.filter(Post.from_bot == False, Post.nsfw == False, Post.nsfl == False, Post.deleted == False)
|
||||
content_filters = {}
|
||||
else:
|
||||
if current_user.ignore_bots:
|
||||
|
@ -207,6 +207,7 @@ def show_community(community: Community):
|
|||
if current_user.show_nsfw is False:
|
||||
posts = posts.filter(Post.nsfw == False)
|
||||
content_filters = user_filters_posts(current_user.id)
|
||||
posts = posts.filter(Post.deleted == False)
|
||||
|
||||
# filter domains and instances
|
||||
domains_ids = blocked_domains(current_user.id)
|
||||
|
@ -319,7 +320,7 @@ def show_community_rss(actor):
|
|||
if request_etag_matches(current_etag):
|
||||
return return_304(current_etag, 'application/rss+xml')
|
||||
|
||||
posts = community.posts.filter(Post.from_bot == False).order_by(desc(Post.created_at)).limit(100).all()
|
||||
posts = community.posts.filter(Post.from_bot == False, Post.deleted == False).order_by(desc(Post.created_at)).limit(100).all()
|
||||
description = shorten_string(community.description, 150) if community.description else None
|
||||
og_image = community.image.source_url if community.image_id else None
|
||||
fg = FeedGenerator()
|
||||
|
|
|
@ -541,7 +541,7 @@ def delete_post_from_community_task(post_id):
|
|||
post = Post.query.get(post_id)
|
||||
community = post.community
|
||||
post.delete_dependencies()
|
||||
db.session.delete(post)
|
||||
post.deleted = True
|
||||
db.session.commit()
|
||||
|
||||
if not community.local_only:
|
||||
|
@ -600,7 +600,7 @@ def delete_post_reply_from_community_task(post_reply_id):
|
|||
post_reply.body_html = markdown_to_html(post_reply.body)
|
||||
else:
|
||||
post_reply.delete_dependencies()
|
||||
db.session.delete(post_reply)
|
||||
post_reply.deleted = True
|
||||
db.session.commit()
|
||||
|
||||
# federate delete
|
||||
|
|
|
@ -26,10 +26,10 @@ def show_domain(domain_id):
|
|||
if domain:
|
||||
if current_user.is_anonymous or current_user.ignore_bots:
|
||||
posts = Post.query.join(Community, Community.id == Post.community_id).\
|
||||
filter(Post.from_bot == False, Post.domain_id == domain.id, Community.banned == False).\
|
||||
filter(Post.from_bot == False, Post.domain_id == domain.id, Community.banned == False, Post.deleted == False).\
|
||||
order_by(desc(Post.posted_at))
|
||||
else:
|
||||
posts = Post.query.join(Community).filter(Post.domain_id == domain.id, Community.banned == False).order_by(desc(Post.posted_at))
|
||||
posts = Post.query.join(Community).filter(Post.domain_id == domain.id, Community.banned == False, Post.deleted == False).order_by(desc(Post.posted_at))
|
||||
|
||||
if current_user.is_authenticated:
|
||||
instance_ids = blocked_instances(current_user.id)
|
||||
|
|
|
@ -30,7 +30,7 @@ from app.utils import render_template, get_setting, gibberish, request_etag_matc
|
|||
blocked_instances, communities_banned_from, topic_tree, recently_upvoted_posts, recently_downvoted_posts, \
|
||||
generate_image_from_video_url, blocked_users, microblog_content_to_title, menu_topics
|
||||
from app.models import Community, CommunityMember, Post, Site, User, utcnow, Domain, Topic, File, Instance, \
|
||||
InstanceRole, Notification, Language, community_language
|
||||
InstanceRole, Notification, Language, community_language, PostReply
|
||||
from PIL import Image
|
||||
import pytesseract
|
||||
|
||||
|
@ -73,7 +73,7 @@ def home_page(type, sort):
|
|||
|
||||
if current_user.is_anonymous:
|
||||
flash(_('Create an account to tailor this feed to your interests.'))
|
||||
posts = Post.query.filter(Post.from_bot == False, Post.nsfw == False, Post.nsfl == False)
|
||||
posts = Post.query.filter(Post.from_bot == False, Post.nsfw == False, Post.nsfl == False, Post.deleted == False)
|
||||
posts = posts.join(Community, Community.id == Post.community_id)
|
||||
if type == 'home':
|
||||
posts = posts.filter(Community.show_home == True)
|
||||
|
@ -368,7 +368,7 @@ def robots():
|
|||
@bp.route('/sitemap.xml')
|
||||
@cache.cached(timeout=6000)
|
||||
def sitemap():
|
||||
posts = Post.query.filter(Post.from_bot == False)
|
||||
posts = Post.query.filter(Post.from_bot == False, Post.deleted == False)
|
||||
posts = posts.join(Community, Community.id == Post.community_id)
|
||||
posts = posts.filter(Community.show_all == True, Community.ap_id == None) # sitemap.xml only includes local posts
|
||||
if not g.site.enable_nsfw:
|
||||
|
@ -396,6 +396,10 @@ def list_files(directory):
|
|||
@bp.route('/test')
|
||||
def test():
|
||||
|
||||
#for community in Community.query.filter(Community.content_retention != -1):
|
||||
# for post in community.posts.filter(Post.posted_at < utcnow() - timedelta(days=Community.content_retention)):
|
||||
# post.delete_dependencies()
|
||||
|
||||
return 'done'
|
||||
|
||||
|
||||
|
|
|
@ -880,7 +880,7 @@ class User(UserMixin, db.Model):
|
|||
db.session.query(Notification).filter(Notification.user_id == self.id).delete()
|
||||
db.session.query(PollChoiceVote).filter(PollChoiceVote.user_id == self.id).delete()
|
||||
|
||||
def purge_content(self):
|
||||
def purge_content(self, soft=True):
|
||||
files = File.query.join(Post).filter(Post.user_id == self.id).all()
|
||||
for file in files:
|
||||
file.delete_from_disk()
|
||||
|
@ -888,12 +888,18 @@ class User(UserMixin, db.Model):
|
|||
posts = Post.query.filter_by(user_id=self.id).all()
|
||||
for post in posts:
|
||||
post.delete_dependencies()
|
||||
db.session.delete(post)
|
||||
if soft:
|
||||
post.deleted = True
|
||||
else:
|
||||
db.session.delete(post)
|
||||
db.session.commit()
|
||||
post_replies = PostReply.query.filter_by(user_id=self.id).all()
|
||||
for reply in post_replies:
|
||||
reply.delete_dependencies()
|
||||
db.session.delete(reply)
|
||||
if soft:
|
||||
reply.deleted = True
|
||||
else:
|
||||
db.session.delete(reply)
|
||||
db.session.commit()
|
||||
|
||||
def mention_tag(self):
|
||||
|
@ -938,6 +944,7 @@ class Post(db.Model):
|
|||
body_html = db.Column(db.Text)
|
||||
type = db.Column(db.Integer)
|
||||
comments_enabled = db.Column(db.Boolean, default=True)
|
||||
deleted = db.Column(db.Boolean, default=False, index=True)
|
||||
mea_culpa = db.Column(db.Boolean, default=False)
|
||||
has_embed = db.Column(db.Boolean, default=False)
|
||||
reply_count = db.Column(db.Integer, default=0)
|
||||
|
@ -1066,6 +1073,7 @@ class PostReply(db.Model):
|
|||
notify_author = db.Column(db.Boolean, default=True)
|
||||
created_at = db.Column(db.DateTime, index=True, default=utcnow)
|
||||
posted_at = db.Column(db.DateTime, index=True, default=utcnow)
|
||||
deleted = db.Column(db.Boolean, default=False, index=True)
|
||||
ip = db.Column(db.String(50))
|
||||
from_bot = db.Column(db.Boolean, default=False)
|
||||
up_votes = db.Column(db.Integer, default=0)
|
||||
|
@ -1142,7 +1150,7 @@ class PostReply(db.Model):
|
|||
return PostReply.query.filter_by(parent_id=self.id).all()
|
||||
|
||||
def has_replies(self):
|
||||
reply = PostReply.query.filter_by(parent_id=self.id).first()
|
||||
reply = PostReply.query.filter_by(parent_id=self.id).filter(PostReply.deleted == False).first()
|
||||
return reply is not None
|
||||
|
||||
def blocked_by_content_filter(self, content_filters):
|
||||
|
|
|
@ -36,7 +36,7 @@ def show_post(post_id: int):
|
|||
post = Post.query.get_or_404(post_id)
|
||||
community: Community = post.community
|
||||
|
||||
if community.banned:
|
||||
if community.banned or post.deleted:
|
||||
abort(404)
|
||||
|
||||
sort = request.args.get('sort', 'hot')
|
||||
|
@ -231,6 +231,7 @@ def show_post(post_id: int):
|
|||
og_image = post.image.source_url if post.image_id else None
|
||||
description = shorten_string(markdown_to_text(post.body), 150) if post.body else None
|
||||
|
||||
# Breadcrumbs
|
||||
breadcrumbs = []
|
||||
breadcrumb = namedtuple("Breadcrumb", ['text', 'url'])
|
||||
breadcrumb.text = _('Home')
|
||||
|
@ -288,15 +289,15 @@ def show_post(post_id: int):
|
|||
poll_total_votes = 0
|
||||
if post.type == POST_TYPE_POLL:
|
||||
poll_data = Poll.query.get(post.id)
|
||||
poll_choices = PollChoice.query.filter_by(post_id=post.id).order_by(PollChoice.sort_order).all()
|
||||
poll_total_votes = poll_data.total_votes()
|
||||
# Show poll results to everyone after the poll finishes, to the poll creator and to those who have voted
|
||||
if (current_user.is_authenticated and (poll_data.has_voted(current_user.id))) \
|
||||
or poll_data.end_poll < datetime.utcnow():
|
||||
|
||||
poll_results = True
|
||||
else:
|
||||
poll_form = True
|
||||
if poll_data:
|
||||
poll_choices = PollChoice.query.filter_by(post_id=post.id).order_by(PollChoice.sort_order).all()
|
||||
poll_total_votes = poll_data.total_votes()
|
||||
# Show poll results to everyone after the poll finishes, to the poll creator and to those who have voted
|
||||
if (current_user.is_authenticated and (poll_data.has_voted(current_user.id))) \
|
||||
or poll_data.end_poll < datetime.utcnow():
|
||||
poll_results = True
|
||||
else:
|
||||
poll_form = True
|
||||
|
||||
response = render_template('post/post.html', title=post.title, post=post, is_moderator=is_moderator, community=post.community,
|
||||
breadcrumbs=breadcrumbs, related_communities=related_communities, mods=mod_list,
|
||||
|
@ -612,7 +613,7 @@ def poll_vote(post_id):
|
|||
def continue_discussion(post_id, comment_id):
|
||||
post = Post.query.get_or_404(post_id)
|
||||
comment = PostReply.query.get_or_404(comment_id)
|
||||
if post.community.banned:
|
||||
if post.community.banned or post.deleted or comment.deleted:
|
||||
abort(404)
|
||||
mods = post.community.moderators()
|
||||
is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods)
|
||||
|
@ -844,6 +845,10 @@ def add_reply(post_id: int, comment_id: int):
|
|||
@bp.route('/post/<int:post_id>/options', methods=['GET'])
|
||||
def post_options(post_id: int):
|
||||
post = Post.query.get_or_404(post_id)
|
||||
if current_user.is_anonymous or not current_user.is_admin():
|
||||
if post.deleted:
|
||||
abort(404)
|
||||
|
||||
return render_template('post/post_options.html', post=post,
|
||||
moderating_communities=moderating_communities(current_user.get_id()),
|
||||
joined_communities=joined_communities(current_user.get_id()),
|
||||
|
@ -854,6 +859,9 @@ def post_options(post_id: int):
|
|||
def post_reply_options(post_id: int, comment_id: int):
|
||||
post = Post.query.get_or_404(post_id)
|
||||
post_reply = PostReply.query.get_or_404(comment_id)
|
||||
if current_user.is_anonymous or not current_user.is_admin():
|
||||
if post.deleted or post_reply.deleted:
|
||||
abort(404)
|
||||
return render_template('post/post_reply_options.html', post=post, post_reply=post_reply,
|
||||
moderating_communities=moderating_communities(current_user.get_id()),
|
||||
joined_communities=joined_communities(current_user.get_id()),
|
||||
|
@ -1467,7 +1475,7 @@ def post_delete(post_id: int):
|
|||
if ocp.cross_posts is not None:
|
||||
ocp.cross_posts.remove(post.id)
|
||||
post.delete_dependencies()
|
||||
db.session.delete(post)
|
||||
post.deleted = True
|
||||
g.site.last_active = community.last_active = utcnow()
|
||||
db.session.commit()
|
||||
flash(_('Post deleted.'))
|
||||
|
@ -1524,6 +1532,61 @@ 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))
|
||||
|
||||
|
||||
@bp.route('/post/<int:post_id>/restore', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def post_restore(post_id: int):
|
||||
post = Post.query.get_or_404(post_id)
|
||||
if post.community.is_moderator() or post.community.is_owner() or current_user.is_admin():
|
||||
post.deleted = False
|
||||
db.session.commit()
|
||||
|
||||
# Federate un-delete
|
||||
if post.is_local():
|
||||
delete_json = {
|
||||
"actor": current_user.profile_id(),
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"object": {
|
||||
'id': f"https://{current_app.config['SERVER_NAME']}/activities/delete/{gibberish(15)}",
|
||||
'type': 'Delete',
|
||||
'actor': current_user.profile_id(),
|
||||
'audience': post.community.profile_id(),
|
||||
'to': [post.community.profile_id(), 'https://www.w3.org/ns/activitystreams#Public'],
|
||||
'published': ap_datetime(utcnow()),
|
||||
'cc': [
|
||||
current_user.followers_url()
|
||||
],
|
||||
'object': post.ap_id,
|
||||
'uri': post.ap_id,
|
||||
"summary": "bad post",
|
||||
},
|
||||
"cc": [post.community.profile_id()],
|
||||
"audience": post.author.profile_id(),
|
||||
"type": "Undo",
|
||||
"id": f"https://{current_app.config['SERVER_NAME']}/activities/undo/{gibberish(15)}"
|
||||
}
|
||||
|
||||
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': 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)
|
||||
|
||||
flash(_('Post has been restored.'))
|
||||
return redirect(url_for('activitypub.post_ap', post_id=post.id))
|
||||
|
||||
|
||||
@bp.route('/post/<int:post_id>/report', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def post_report(post_id: int):
|
||||
|
@ -1906,7 +1969,7 @@ def post_reply_delete(post_id: int, comment_id: int):
|
|||
post_reply.body_html = markdown_to_html(post_reply.body)
|
||||
else:
|
||||
post_reply.delete_dependencies()
|
||||
db.session.delete(post_reply)
|
||||
post_reply.deleted = True
|
||||
g.site.last_active = community.last_active = utcnow()
|
||||
db.session.commit()
|
||||
flash(_('Comment deleted.'))
|
||||
|
|
|
@ -10,7 +10,7 @@ from app.utils import blocked_instances, blocked_users
|
|||
|
||||
# replies to a post, in a tree, sorted by a variety of methods
|
||||
def post_replies(post_id: int, sort_by: str, show_first: int = 0) -> List[PostReply]:
|
||||
comments = PostReply.query.filter_by(post_id=post_id)
|
||||
comments = PostReply.query.filter_by(post_id=post_id).filter(PostReply.deleted == False)
|
||||
if current_user.is_authenticated:
|
||||
instance_ids = blocked_instances(current_user.id)
|
||||
if instance_ids:
|
||||
|
@ -46,7 +46,7 @@ def get_comment_branch(post_id: int, comment_id: int, sort_by: str) -> List[Post
|
|||
if parent_comment is None:
|
||||
return []
|
||||
|
||||
comments = PostReply.query.filter(PostReply.post_id == post_id)
|
||||
comments = PostReply.query.filter(PostReply.post_id == post_id, PostReply.deleted == False)
|
||||
if current_user.is_authenticated:
|
||||
instance_ids = blocked_instances(current_user.id)
|
||||
if instance_ids:
|
||||
|
@ -71,7 +71,7 @@ def get_comment_branch(post_id: int, comment_id: int, sort_by: str) -> List[Post
|
|||
|
||||
# The number of replies a post has
|
||||
def post_reply_count(post_id) -> int:
|
||||
return db.session.execute(text('SELECT COUNT(id) as c FROM "post_reply" WHERE post_id = :post_id'),
|
||||
return db.session.execute(text('SELECT COUNT(id) as c FROM "post_reply" WHERE post_id = :post_id AND deleted is false'),
|
||||
{'post_id': post_id}).scalar()
|
||||
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ def show_tag(tag):
|
|||
|
||||
posts = Post.query.join(Community, Community.id == Post.community_id). \
|
||||
join(post_tag, post_tag.c.post_id == Post.id).filter(post_tag.c.tag_id == tag.id). \
|
||||
filter(Community.banned == False)
|
||||
filter(Community.banned == False, Post.deleted == False)
|
||||
|
||||
if current_user.is_anonymous or current_user.ignore_bots:
|
||||
posts = posts.filter(Post.from_bot == False)
|
||||
|
|
41
app/templates/admin/deleted_posts.html
Normal file
41
app/templates/admin/deleted_posts.html
Normal file
|
@ -0,0 +1,41 @@
|
|||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
||||
{% extends 'themes/' + theme() + '/base.html' %}
|
||||
{% else %}
|
||||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
{% set active_child = 'admin_content_deleted' %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>{{ _('Deleted posts') }}</h1>
|
||||
<div class="post_list">
|
||||
{% for post in posts.items %}
|
||||
{% include 'post/_post_teaser.html' %}
|
||||
{% else %}
|
||||
<p>{{ _('No deleted posts.') }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if post_replies %}
|
||||
<h2 class="mt-4" id="comments">Deleted comments</h2>
|
||||
<div class="post_list">
|
||||
{% for post_reply in post_replies.items %}
|
||||
{% include 'post/_post_reply_teaser.html' %}
|
||||
{% else %}
|
||||
<p>{{ _('No deleted comments.') }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>{{ _('No comments yet.') }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -213,6 +213,7 @@
|
|||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_users_trash' }}" href="{{ url_for('admin.admin_users_trash', local_remote='local') }}">{{ _('Monitoring - users') }}</a></li>
|
||||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_content_trash' }}" href="{{ url_for('admin.admin_content_trash') }}">{{ _('Monitoring - content') }}</a></li>
|
||||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_content_spam' }}" href="{{ url_for('admin.admin_content_spam') }}">{{ _('Monitoring - spammy content') }}</a></li>
|
||||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_content_deleted' }}" href="{{ url_for('admin.admin_content_deleted') }}">{{ _('Deleted content') }}</a></li>
|
||||
{% if g.site.registration_mode == 'RequireApplication' %}
|
||||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_approve_registrations' }}" href="{{ url_for('admin.admin_approve_registrations') }}">{{ _('Registration applications') }}</a></li>
|
||||
{% endif %}
|
||||
|
|
|
@ -1,51 +1,56 @@
|
|||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
||||
{% extends 'themes/' + theme() + '/base.html' %}
|
||||
{% else %}
|
||||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') -%}
|
||||
{% extends 'themes/' + theme() + '/base.html' -%}
|
||||
{% else -%}
|
||||
{% extends "base.html" -%}
|
||||
{% endif -%} -%}
|
||||
{% from 'bootstrap/form.html' import render_form -%}
|
||||
|
||||
{% block app_content %}
|
||||
{% block app_content -%}
|
||||
<div class="row">
|
||||
<div class="col col-login mx-auto">
|
||||
<div class="card mt-5">
|
||||
<div class="card-body p-6">
|
||||
<div class="card-title">{{ _('Options for "%(post_title)s"', post_title=post.title) }}</div>
|
||||
<ul class="option_list">
|
||||
{% if current_user.is_authenticated %}
|
||||
{% if post.user_id == current_user.id %}
|
||||
{% if current_user.is_authenticated -%}
|
||||
{% if post.user_id == current_user.id -%}
|
||||
<li><a href="{{ url_for('post.post_edit', post_id=post.id) }}" class="no-underline" rel="nofollow"><span class="fe fe-edit"></span>
|
||||
{{ _('Edit') }}</a></li>
|
||||
{% endif %}
|
||||
{% if post.user_id == current_user.id or post.community.is_moderator() or post.community.is_owner() or current_user.is_admin() %}
|
||||
<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>
|
||||
{% endif %}
|
||||
{% if post.user_id == current_user.id and not post.mea_culpa %}
|
||||
{% 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.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>
|
||||
{{ _('Restore') }}</a></li>
|
||||
{% 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>
|
||||
{{ _('Delete') }}</a></li>
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
{% if post.user_id == current_user.id and not post.mea_culpa -%}
|
||||
<li><a href="{{ url_for('post.post_mea_culpa', post_id=post.id) }}" class="no-underline"><span class="fe fe-mea-culpa"></span>
|
||||
{{ _("I made a mistake with this post and have changed my mind about the topic") }}</a></li>
|
||||
{% endif %}
|
||||
{% if post.user_id != current_user.id %}
|
||||
{% endif -%}
|
||||
{% if post.user_id != current_user.id -%}
|
||||
<li><a href="{{ url_for('post.post_block_user', post_id=post.id) }}" class="no-underline"><span class="fe fe-block"></span>
|
||||
{{ _('Block post author @%(author_name)s', author_name=post.author.user_name) }}</a></li>
|
||||
{% if post.community.is_moderator() or current_user.is_admin() %}
|
||||
{% if post.community.is_moderator() or current_user.is_admin() -%}
|
||||
<li><a href="{{ url_for('community.community_ban_user', community_id=post.community.id, user_id=post.author.id) }}" class="no-underline"><span class="fe fe-block red"></span>
|
||||
{{ _('Ban post author @%(author_name)s from<br>%(community_name)s', author_name=post.author.user_name, community_name=post.community.title) }}</a></li>
|
||||
{% endif %}
|
||||
{% if post.domain_id %}
|
||||
{% endif -%}
|
||||
{% if post.domain_id -%}
|
||||
<li><a href="{{ url_for('post.post_block_domain', post_id=post.id) }}" class="no-underline"><span class="fe fe-block"></span>
|
||||
{{ _('Block domain %(domain)s', domain=post.domain.name) }}</a></li>
|
||||
{% endif %}
|
||||
{% if post.instance_id and post.instance_id != 1 %}
|
||||
{% endif -%}
|
||||
{% if post.instance_id and post.instance_id != 1 -%}
|
||||
<li><a href="{{ url_for('post.post_block_instance', post_id=post.id) }}" class="no-underline"><span class="fe fe-block"></span>
|
||||
{{ _("Hide every post from author's instance: %(name)s", name=post.instance.domain) }}</a></li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if post.ap_id %}
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
{% if post.ap_id -%}
|
||||
<li><a href="{{ post.ap_id }}" rel="nofollow" class="no-underline"><span class="fe fe-external"></span>
|
||||
{{ _('View original on %(domain)s', domain=post.instance.domain) }}</a></li>
|
||||
{% endif %}
|
||||
{% endif -%}
|
||||
<li><a href="{{ url_for('post.post_report', post_id=post.id) }}" class="no-underline" rel="nofollow"><span class="fe fe-report"></span>
|
||||
{{ _('Report to moderators') }}</a></li>
|
||||
</ul>
|
||||
|
@ -54,4 +59,4 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock -%}
|
|
@ -1,35 +1,40 @@
|
|||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
||||
{% extends 'themes/' + theme() + '/base.html' %}
|
||||
{% else %}
|
||||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') -%}
|
||||
{% extends 'themes/' + theme() + '/base.html' -%}
|
||||
{% else -%}
|
||||
{% extends "base.html" -%}
|
||||
{% endif -%} -%}
|
||||
{% from 'bootstrap/form.html' import render_form -%}
|
||||
|
||||
{% block app_content %}
|
||||
{% block app_content -%}
|
||||
<div class="row">
|
||||
<div class="col col-login mx-auto">
|
||||
<div class="card mt-5">
|
||||
<div class="card-body p-6">
|
||||
<div class="card-title">{{ _('Options for comment on "%(post_title)s"', post_title=post.title) }}</div>
|
||||
<ul class="option_list">
|
||||
{% if current_user.is_authenticated %}
|
||||
{% if post_reply.user_id == current_user.id %}
|
||||
{% if current_user.is_authenticated -%}
|
||||
{% if post_reply.user_id == current_user.id -%}
|
||||
<li><a href="{{ url_for('post.post_reply_edit', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline" rel="nofollow"><span class="fe fe-edit"></span>
|
||||
{{ _('Edit') }}</a></li>
|
||||
{% endif %}
|
||||
{% if post_reply.user_id == current_user.id or post.community.is_moderator() or post.community.is_owner() or current_user.is_admin() %}
|
||||
<li><a href="{{ url_for('post.post_reply_delete', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-delete"></span>
|
||||
{{ _('Delete') }}</a></li>
|
||||
{% endif %}
|
||||
{% if post_reply.user_id != current_user.id %}
|
||||
{% endif -%}
|
||||
{% if post_reply.user_id == current_user.id or post.community.is_moderator() or post.community.is_owner() or current_user.is_admin() -%}
|
||||
{% if post_reply.deleted -%}
|
||||
<li><a href="{{ url_for('post.post_reply_restore', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-delete"></span>
|
||||
{{ _('Restore') }}</a></li>
|
||||
{% else -%}
|
||||
<li><a href="{{ url_for('post.post_reply_delete', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-delete"></span>
|
||||
{{ _('Delete') }}</a></li>
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
{% if post_reply.user_id != current_user.id -%}
|
||||
<li><a href="{{ url_for('post.post_reply_block_user', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline"><span class="fe fe-block"></span>
|
||||
{{ _('Block author @%(author_name)s', author_name=post_reply.author.user_name) }}</a></li>
|
||||
{% if post_reply.instance_id and post_reply.instance_id != 1 %}
|
||||
{% if post_reply.instance_id and post_reply.instance_id != 1 -%}
|
||||
<li><a href="{{ url_for('post.post_reply_block_instance', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline"><span class="fe fe-block"></span>
|
||||
{{ _("Hide every post from author's instance: %(name)s", name=post_reply.instance.domain) }}</a></li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
<li><a href="{{ url_for('post.post_reply_report', post_id=post.id, comment_id=post_reply.id) }}" rel="nofollow" class="no-underline"><span class="fe fe-report"></span>
|
||||
{{ _('Report to moderators') }}</a></li>
|
||||
</ul>
|
||||
|
@ -38,4 +43,4 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock -%}
|
|
@ -54,7 +54,7 @@ def show_topic(topic_path):
|
|||
|
||||
# filter out nsfw and nsfl if desired
|
||||
if current_user.is_anonymous:
|
||||
posts = posts.filter(Post.from_bot == False, Post.nsfw == False, Post.nsfl == False)
|
||||
posts = posts.filter(Post.from_bot == False, Post.nsfw == False, Post.nsfl == False, Post.deleted == False)
|
||||
content_filters = {}
|
||||
else:
|
||||
if current_user.ignore_bots:
|
||||
|
@ -63,6 +63,7 @@ def show_topic(topic_path):
|
|||
posts = posts.filter(Post.nsfl == False)
|
||||
if current_user.show_nsfw is False:
|
||||
posts = posts.filter(Post.nsfw == False)
|
||||
posts = posts.filter(Post.deleted == False)
|
||||
content_filters = user_filters_posts(current_user.id)
|
||||
|
||||
# filter blocked domains and instances
|
||||
|
@ -138,7 +139,7 @@ def show_topic_rss(topic_path):
|
|||
if topic:
|
||||
posts = Post.query.join(Community, Post.community_id == Community.id).filter(Community.topic_id == topic.id,
|
||||
Community.banned == False)
|
||||
posts = posts.filter(Post.from_bot == False, Post.nsfw == False, Post.nsfl == False)
|
||||
posts = posts.filter(Post.from_bot == False, Post.nsfw == False, Post.nsfl == False, Post.deleted == False)
|
||||
posts = posts.order_by(desc(Post.created_at)).limit(100).all()
|
||||
|
||||
fg = FeedGenerator()
|
||||
|
|
|
@ -59,7 +59,7 @@ def show_profile(user):
|
|||
post_page = request.args.get('post_page', 1, type=int)
|
||||
replies_page = request.args.get('replies_page', 1, type=int)
|
||||
|
||||
posts = Post.query.filter_by(user_id=user.id).order_by(desc(Post.posted_at)).paginate(page=post_page, per_page=50, error_out=False)
|
||||
posts = Post.query.filter_by(user_id=user.id).filter(Post.deleted == False).order_by(desc(Post.posted_at)).paginate(page=post_page, per_page=50, error_out=False)
|
||||
moderates = Community.query.filter_by(banned=False).join(CommunityMember).filter(CommunityMember.user_id == user.id)\
|
||||
.filter(or_(CommunityMember.is_moderator, CommunityMember.is_owner))
|
||||
if current_user.is_authenticated and (user.id == current_user.get_id() or current_user.is_admin()):
|
||||
|
@ -69,7 +69,7 @@ def show_profile(user):
|
|||
subscribed = Community.query.filter_by(banned=False).join(CommunityMember).filter(CommunityMember.user_id == user.id).all()
|
||||
if current_user.is_anonymous or user.id != current_user.id:
|
||||
moderates = moderates.filter(Community.private_mods == False)
|
||||
post_replies = PostReply.query.filter_by(user_id=user.id).order_by(desc(PostReply.posted_at)).paginate(page=replies_page, per_page=50, error_out=False)
|
||||
post_replies = PostReply.query.filter_by(user_id=user.id).filter(PostReply.deleted == False).order_by(desc(PostReply.posted_at)).paginate(page=replies_page, per_page=50, error_out=False)
|
||||
|
||||
# profile info
|
||||
canonical = user.ap_public_url if user.ap_public_url else None
|
||||
|
|
45
migrations/versions/5e84668d279e_soft_deletes.py
Normal file
45
migrations/versions/5e84668d279e_soft_deletes.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
"""soft deletes
|
||||
|
||||
Revision ID: 5e84668d279e
|
||||
Revises: 2191fa36c09d
|
||||
Create Date: 2024-06-02 14:50:17.295862
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '5e84668d279e'
|
||||
down_revision = '2191fa36c09d'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('post', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('deleted', sa.Boolean(), nullable=True))
|
||||
batch_op.create_index(batch_op.f('ix_post_deleted'), ['deleted'], unique=False)
|
||||
|
||||
with op.batch_alter_table('post_reply', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('deleted', sa.Boolean(), nullable=True))
|
||||
batch_op.create_index(batch_op.f('ix_post_reply_deleted'), ['deleted'], unique=False)
|
||||
|
||||
op.execute(sa.DDL('UPDATE "post" SET deleted = false'))
|
||||
op.execute(sa.DDL('UPDATE "post_reply" SET deleted = false'))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('post_reply', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_post_reply_deleted'))
|
||||
batch_op.drop_column('deleted')
|
||||
|
||||
with op.batch_alter_table('post', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_post_deleted'))
|
||||
batch_op.drop_column('deleted')
|
||||
|
||||
# ### end Alembic commands ###
|
Loading…
Reference in a new issue