mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 11:26:56 -08:00
user banning and purging
This commit is contained in:
parent
4b916fcf86
commit
56d09b264a
17 changed files with 285 additions and 69 deletions
|
@ -138,6 +138,8 @@ def user_profile(actor):
|
||||||
return resp
|
return resp
|
||||||
else:
|
else:
|
||||||
return show_profile(user)
|
return show_profile(user)
|
||||||
|
else:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/c/<actor>', methods=['GET'])
|
@bp.route('/c/<actor>', methods=['GET'])
|
||||||
|
@ -313,38 +315,39 @@ def shared_inbox():
|
||||||
user_ap_id = request_json['object']['actor']
|
user_ap_id = request_json['object']['actor']
|
||||||
liked_ap_id = request_json['object']['object']
|
liked_ap_id = request_json['object']['object']
|
||||||
user = find_actor_or_create(user_ap_id)
|
user = find_actor_or_create(user_ap_id)
|
||||||
vote_weight = 1.0
|
if user:
|
||||||
if user.ap_domain:
|
vote_weight = 1.0
|
||||||
instance = Instance.query.filter_by(domain=user.ap_domain).fetch()
|
if user.ap_domain:
|
||||||
if instance:
|
instance = Instance.query.filter_by(domain=user.ap_domain).fetch()
|
||||||
vote_weight = instance.vote_weight
|
if instance:
|
||||||
liked = find_liked_object(liked_ap_id)
|
vote_weight = instance.vote_weight
|
||||||
# insert into voted table
|
liked = find_liked_object(liked_ap_id)
|
||||||
if liked is not None and isinstance(liked, Post):
|
# insert into voted table
|
||||||
existing_vote = PostVote.query.filter_by(user_id=user.id, post_id=liked.id).first()
|
if liked is not None and isinstance(liked, Post):
|
||||||
if existing_vote:
|
existing_vote = PostVote.query.filter_by(user_id=user.id, post_id=liked.id).first()
|
||||||
existing_vote.effect = vote_effect * vote_weight
|
if existing_vote:
|
||||||
|
existing_vote.effect = vote_effect * vote_weight
|
||||||
|
else:
|
||||||
|
vote = PostVote(user_id=user.id, author_id=liked.user_id, post_id=liked.id,
|
||||||
|
effect=vote_effect * vote_weight)
|
||||||
|
db.session.add(vote)
|
||||||
|
db.session.commit()
|
||||||
|
activity_log.result = 'success'
|
||||||
|
elif liked is not None and isinstance(liked, PostReply):
|
||||||
|
existing_vote = PostVote.query.filter_by(user_id=user.id, post_id=liked.id).first()
|
||||||
|
if existing_vote:
|
||||||
|
existing_vote.effect = vote_effect * vote_weight
|
||||||
|
else:
|
||||||
|
vote = PostReplyVote(user_id=user.id, author_id=liked.user_id, post_reply_id=liked.id,
|
||||||
|
effect=vote_effect * vote_weight)
|
||||||
|
db.session.add(vote)
|
||||||
|
db.session.commit()
|
||||||
|
activity_log.result = 'success'
|
||||||
else:
|
else:
|
||||||
vote = PostVote(user_id=user.id, author_id=liked.user_id, post_id=liked.id,
|
activity_log.exception_message = 'Could not detect type of like'
|
||||||
effect=vote_effect * vote_weight)
|
if activity_log.result == 'success':
|
||||||
db.session.add(vote)
|
... # todo: recalculate 'hotness' of liked post/reply
|
||||||
db.session.commit()
|
# todo: if vote was on content in local community, federate the vote out to followers
|
||||||
activity_log.result = 'success'
|
|
||||||
elif liked is not None and isinstance(liked, PostReply):
|
|
||||||
existing_vote = PostVote.query.filter_by(user_id=user.id, post_id=liked.id).first()
|
|
||||||
if existing_vote:
|
|
||||||
existing_vote.effect = vote_effect * vote_weight
|
|
||||||
else:
|
|
||||||
vote = PostReplyVote(user_id=user.id, author_id=liked.user_id, post_reply_id=liked.id,
|
|
||||||
effect=vote_effect * vote_weight)
|
|
||||||
db.session.add(vote)
|
|
||||||
db.session.commit()
|
|
||||||
activity_log.result = 'success'
|
|
||||||
else:
|
|
||||||
activity_log.exception_message = 'Could not detect type of like'
|
|
||||||
if activity_log.result == 'success':
|
|
||||||
... # todo: recalculate 'hotness' of liked post/reply
|
|
||||||
# todo: if vote was on content in local community, federate the vote out to followers
|
|
||||||
|
|
||||||
# Follow: remote user wants to follow one of our communities
|
# Follow: remote user wants to 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
|
||||||
|
@ -399,14 +402,15 @@ def shared_inbox():
|
||||||
user_ap_id = request_json['object']['actor']
|
user_ap_id = request_json['object']['actor']
|
||||||
user = find_actor_or_create(user_ap_id)
|
user = find_actor_or_create(user_ap_id)
|
||||||
community = find_actor_or_create(community_ap_id)
|
community = find_actor_or_create(community_ap_id)
|
||||||
join_request = CommunityJoinRequest.query.filter_by(user_id=user.id,
|
if user and community:
|
||||||
community_id=community.id).first()
|
join_request = CommunityJoinRequest.query.filter_by(user_id=user.id,
|
||||||
if join_request:
|
community_id=community.id).first()
|
||||||
member = CommunityMember(user_id=user.id, community_id=community.id)
|
if join_request:
|
||||||
db.session.add(member)
|
member = CommunityMember(user_id=user.id, community_id=community.id)
|
||||||
community.subscriptions_count += 1
|
db.session.add(member)
|
||||||
db.session.commit()
|
community.subscriptions_count += 1
|
||||||
activity_log.result = 'success'
|
db.session.commit()
|
||||||
|
activity_log.result = 'success'
|
||||||
else:
|
else:
|
||||||
activity_log.exception_message = 'Instance banned'
|
activity_log.exception_message = 'Instance banned'
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -192,7 +192,7 @@ def find_actor_or_create(actor: str) -> Union[User, Community, None]:
|
||||||
ap_profile_id=actor).first() # finds communities formatted like https://localhost/c/*
|
ap_profile_id=actor).first() # finds communities formatted like https://localhost/c/*
|
||||||
|
|
||||||
if current_app.config['SERVER_NAME'] + '/u/' in actor:
|
if current_app.config['SERVER_NAME'] + '/u/' in actor:
|
||||||
user = User.query.filter_by(username=actor.split('/')[-1], ap_id=None).first() # finds local users
|
user = User.query.filter_by(username=actor.split('/')[-1], ap_id=None, banned=False).first() # finds local users
|
||||||
if user is None:
|
if user is None:
|
||||||
return None
|
return None
|
||||||
elif actor.startswith('https://'):
|
elif actor.startswith('https://'):
|
||||||
|
@ -201,6 +201,8 @@ def find_actor_or_create(actor: str) -> Union[User, Community, None]:
|
||||||
return None
|
return None
|
||||||
user = User.query.filter_by(
|
user = User.query.filter_by(
|
||||||
ap_profile_id=actor).first() # finds users formatted like https://kbin.social/u/tables
|
ap_profile_id=actor).first() # finds users formatted like https://kbin.social/u/tables
|
||||||
|
if user.banned:
|
||||||
|
return None
|
||||||
if user is None:
|
if user is None:
|
||||||
user = Community.query.filter_by(ap_profile_id=actor).first()
|
user = Community.query.filter_by(ap_profile_id=actor).first()
|
||||||
if user is None:
|
if user is None:
|
||||||
|
|
|
@ -48,4 +48,4 @@ class ResetPasswordForm(FlaskForm):
|
||||||
password2 = PasswordField(
|
password2 = PasswordField(
|
||||||
_l('Repeat password'), validators=[DataRequired(),
|
_l('Repeat password'), validators=[DataRequired(),
|
||||||
EqualTo('password')])
|
EqualTo('password')])
|
||||||
submit = SubmitField(_l('Request password reset'))
|
submit = SubmitField(_l('Set password'))
|
||||||
|
|
|
@ -86,11 +86,13 @@ def register(app):
|
||||||
|
|
||||||
staff_role = Role(name='Staff', weight=2)
|
staff_role = Role(name='Staff', weight=2)
|
||||||
staff_role.permissions.append(RolePermission(permission='approve registrations'))
|
staff_role.permissions.append(RolePermission(permission='approve registrations'))
|
||||||
staff_role.permissions.append(RolePermission(permission='manage users'))
|
staff_role.permissions.append(RolePermission(permission='ban users'))
|
||||||
db.session.add(staff_role)
|
db.session.add(staff_role)
|
||||||
|
|
||||||
admin_role = Role(name='Admin', weight=3)
|
admin_role = Role(name='Admin', weight=3)
|
||||||
|
admin_role.permissions.append(RolePermission(permission='approve registrations'))
|
||||||
admin_role.permissions.append(RolePermission(permission='change user roles'))
|
admin_role.permissions.append(RolePermission(permission='change user roles'))
|
||||||
|
admin_role.permissions.append(RolePermission(permission='ban users'))
|
||||||
admin_role.permissions.append(RolePermission(permission='manage users'))
|
admin_role.permissions.append(RolePermission(permission='manage users'))
|
||||||
db.session.add(admin_role)
|
db.session.add(admin_role)
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,9 @@ from hashlib import md5
|
||||||
from time import time
|
from time import time
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from flask import current_app, escape
|
from flask import current_app, escape, url_for
|
||||||
from flask_login import UserMixin
|
from flask_login import UserMixin
|
||||||
from sqlalchemy import or_
|
from sqlalchemy import or_, text
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
from flask_babel import _, lazy_gettext as _l
|
from flask_babel import _, lazy_gettext as _l
|
||||||
from sqlalchemy.orm import backref
|
from sqlalchemy.orm import backref
|
||||||
|
@ -38,6 +38,7 @@ class Community(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
icon_id = db.Column(db.Integer, db.ForeignKey('file.id'))
|
icon_id = db.Column(db.Integer, db.ForeignKey('file.id'))
|
||||||
image_id = db.Column(db.Integer, db.ForeignKey('file.id'))
|
image_id = db.Column(db.Integer, db.ForeignKey('file.id'))
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||||
name = db.Column(db.String(256), index=True)
|
name = db.Column(db.String(256), index=True)
|
||||||
title = db.Column(db.String(256))
|
title = db.Column(db.String(256))
|
||||||
description = db.Column(db.Text)
|
description = db.Column(db.Text)
|
||||||
|
@ -185,6 +186,12 @@ class User(UserMixin, db.Model):
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def display_name(self):
|
||||||
|
if self.deleted is False:
|
||||||
|
return self.user_name
|
||||||
|
else:
|
||||||
|
return '[deleted]'
|
||||||
|
|
||||||
def avatar(self, size):
|
def avatar(self, size):
|
||||||
digest = md5(self.email.lower().encode('utf-8')).hexdigest()
|
digest = md5(self.email.lower().encode('utf-8')).hexdigest()
|
||||||
return 'https://www.gravatar.com/avatar/{}?d=identicon&s={}'.format(
|
return 'https://www.gravatar.com/avatar/{}?d=identicon&s={}'.format(
|
||||||
|
@ -266,6 +273,30 @@ class User(UserMixin, db.Model):
|
||||||
return
|
return
|
||||||
return User.query.get(id)
|
return User.query.get(id)
|
||||||
|
|
||||||
|
def purge_content(self):
|
||||||
|
db.session.query(ActivityLog).filter(ActivityLog.user_id == self.id).delete()
|
||||||
|
db.session.query(PostVote).filter(PostVote.user_id == self.id).delete()
|
||||||
|
db.session.query(PostReplyVote).filter(PostReplyVote.user_id == self.id).delete()
|
||||||
|
db.session.query(PostReply).filter(PostReply.user_id == self.id).delete()
|
||||||
|
db.session.query(FilterKeyword).filter(FilterKeyword.user_id == self.id).delete()
|
||||||
|
db.session.query(Filter).filter(Filter.user_id == self.id).delete()
|
||||||
|
db.session.query(DomainBlock).filter(DomainBlock.user_id == self.id).delete()
|
||||||
|
db.session.query(CommunityJoinRequest).filter(CommunityJoinRequest.user_id == self.id).delete()
|
||||||
|
db.session.query(CommunityMember).filter(CommunityMember.user_id == self.id).delete()
|
||||||
|
db.session.query(CommunityBlock).filter(CommunityBlock.user_id == self.id).delete()
|
||||||
|
db.session.query(CommunityBan).filter(CommunityBan.user_id == self.id).delete()
|
||||||
|
db.session.query(Community).filter(Community.user_id == self.id).delete()
|
||||||
|
db.session.query(Post).filter(Post.user_id == self.id).delete()
|
||||||
|
db.session.query(UserNote).filter(UserNote.user_id == self.id).delete()
|
||||||
|
db.session.query(UserNote).filter(UserNote.target_id == self.id).delete()
|
||||||
|
db.session.query(UserFollowRequest).filter(UserFollowRequest.follow_id == self.id).delete()
|
||||||
|
db.session.query(UserFollowRequest).filter(UserFollowRequest.user_id == self.id).delete()
|
||||||
|
db.session.query(UserBlock).filter(UserBlock.blocked_id == self.id).delete()
|
||||||
|
db.session.query(UserBlock).filter(UserBlock.blocker_id == self.id).delete()
|
||||||
|
db.session.execute(text('DELETE FROM user_role WHERE user_id = :user_id'),
|
||||||
|
{'user_id': self.id})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ActivityLog(db.Model):
|
class ActivityLog(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
setupCommunityNameInput();
|
setupCommunityNameInput();
|
||||||
setupShowMoreLinks();
|
setupShowMoreLinks();
|
||||||
|
setupConfirmFirst();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,6 +12,17 @@ window.addEventListener("load", function () {
|
||||||
setupHideButtons();
|
setupHideButtons();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// every element with the 'confirm_first' class gets a popup confirmation dialog
|
||||||
|
function setupConfirmFirst() {
|
||||||
|
const show_first = document.querySelectorAll('.confirm_first');
|
||||||
|
show_first.forEach(element => {
|
||||||
|
element.addEventListener("click", function(event) {
|
||||||
|
if (!confirm("Are you sure?")) {
|
||||||
|
event.preventDefault(); // As the user clicked "Cancel" in the dialog, prevent the default action.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
function setupShowMoreLinks() {
|
function setupShowMoreLinks() {
|
||||||
const comments = document.querySelectorAll('.comment');
|
const comments = document.querySelectorAll('.comment');
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
|
{% macro render_username(user) %}
|
||||||
|
{% if user.deleted %}
|
||||||
|
[deleted]
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ url_for('activitypub.user_profile', actor=user.user_name) }}">{{ user.user_name }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endmacro %}
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</small></p>
|
</small></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p><small>submitted {{ moment(post.posted_at).fromNow() }} by {{ post.author.user_name }}</small></p>
|
<p><small>submitted {{ moment(post.posted_at).fromNow() }} by {{ render_username(post.author) }}</small></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
{% if post.url %}
|
{% if post.url %}
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
</small></p>
|
</small></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p class="small">submitted {{ moment(post.posted_at).fromNow() }} by
|
<p class="small">submitted {{ moment(post.posted_at).fromNow() }} by
|
||||||
<a href="{{ url_for('activitypub.user_profile', actor=post.author.user_name) }}">{{ post.author.user_name }}</a>
|
{{ render_username(post.author) }}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<div class="post_teaser">
|
<div class="post_teaser">
|
||||||
<div class="row meta_row small">
|
<div class="row meta_row small">
|
||||||
<div class="col"><a href="{{ url_for('activitypub.user_profile', actor=post.author.user_name) }}">{{ post.author.user_name }}</a> · {{ moment(post.posted_at).fromNow() }}</div>
|
<div class="col">{{ render_username(post.author) }} · {{ moment(post.posted_at).fromNow() }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row main_row">
|
<div class="row main_row">
|
||||||
<div class="col{% if post.image_id %}-8{% endif %}">
|
<div class="col{% if post.image_id %}-8{% endif %}">
|
||||||
|
|
|
@ -75,7 +75,7 @@
|
||||||
<h3>Moderators</h3>
|
<h3>Moderators</h3>
|
||||||
<ol>
|
<ol>
|
||||||
{% for mod in mods %}
|
{% for mod in mods %}
|
||||||
<li><a href="/u/{{ mod.user_name }}">{{ mod.user_name }}</a></li>
|
<li>{{ render_username(mod) }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ol>
|
</ol>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -18,12 +18,16 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="hide_button"><a href='#'>[-] hide</a></div>
|
<div class="hide_button"><a href='#'>[-] hide</a></div>
|
||||||
<div class="comment_author">
|
<div class="comment_author">
|
||||||
{% if comment['comment'].author.avatar_id %}
|
{% if comment['comment'].author.deleted %}
|
||||||
<a href="/u/{{ comment['comment'].author.link() }}" title="{{ comment['comment'].author.ap_id }}">
|
<strong>[deleted]</strong>
|
||||||
<img src="{{ comment['comment'].author.avatar_image() }}" alt="Avatar" /></a>
|
{% else %}
|
||||||
{% endif %}
|
{% if comment['comment'].author.avatar_id %}
|
||||||
<a href="/u/{{ comment['comment'].author.link() }}" title="{{ comment['comment'].author.link() }}">
|
<a href="/u/{{ comment['comment'].author.link() }}" title="{{ comment['comment'].author.ap_id }}">
|
||||||
<strong>{{ comment['comment'].author.user_name}}</strong></a>
|
<img src="{{ comment['comment'].author.avatar_image() }}" alt="Avatar" /></a>
|
||||||
|
{% endif %}
|
||||||
|
<a href="/u/{{ comment['comment'].author.link() }}" title="{{ comment['comment'].author.link() }}">
|
||||||
|
<strong>{{ comment['comment'].author.user_name}}</strong></a>
|
||||||
|
{% endif %}
|
||||||
{% if comment['comment'].author.id == post.author.id%}<span title="Submitter of original post" aria-label="submitter">[S]</span>{% endif %}
|
{% if comment['comment'].author.id == post.author.id%}<span title="Submitter of original post" aria-label="submitter">[S]</span>{% endif %}
|
||||||
<span class="text-muted small">{{ moment(comment['comment'].posted_at).fromNow(refresh=True) }}</span>
|
<span class="text-muted small">{{ moment(comment['comment'].posted_at).fromNow(refresh=True) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -30,12 +30,16 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="hide_button"><a href='#'>[-] hide</a></div>
|
<div class="hide_button"><a href='#'>[-] hide</a></div>
|
||||||
<div class="comment_author">
|
<div class="comment_author">
|
||||||
{% if comment['comment'].author.avatar_id %}
|
{% if comment['comment'].author.deleted %}
|
||||||
<a href="/u/{{ comment['comment'].author.link() }}" title="{{ comment['comment'].author.ap_id }}">
|
<strong>[deleted]</strong>
|
||||||
<img src="{{ comment['comment'].author.avatar_image() }}" alt="Avatar" /></a>
|
{% else %}
|
||||||
|
{% if comment['comment'].author.avatar_id %}
|
||||||
|
<a href="/u/{{ comment['comment'].author.link() }}" title="{{ comment['comment'].author.ap_id }}">
|
||||||
|
<img src="{{ comment['comment'].author.avatar_image() }}" alt="Avatar" /></a>
|
||||||
|
{% endif %}
|
||||||
|
<a href="/u/{{ comment['comment'].author.link() }}" title="{{ comment['comment'].author.link() }}">
|
||||||
|
<strong>{{ comment['comment'].author.user_name }}</strong></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="/u/{{ comment['comment'].author.link() }}" title="{{ comment['comment'].author.link() }}">
|
|
||||||
<strong>{{ comment['comment'].author.user_name }}</strong></a>
|
|
||||||
{% if comment['comment'].author.id == post.author.id %}<span title="Submitter of original post" aria-label="submitter">[S]</span>{% endif %}
|
{% if comment['comment'].author.id == post.author.id %}<span title="Submitter of original post" aria-label="submitter">[S]</span>{% endif %}
|
||||||
<span class="text-muted small">{{ moment(comment['comment'].posted_at).fromNow(refresh=True) }}</span>
|
<span class="text-muted small">{{ moment(comment['comment'].posted_at).fromNow(refresh=True) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -57,7 +57,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
{% if current_user.id == user.id %}
|
{% if current_user.is_authenticated and current_user.id == user.id %}
|
||||||
<div class="card mt-3">
|
<div class="card mt-3">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2>{{ _('Manage') }}</h2>
|
<h2>{{ _('Manage') }}</h2>
|
||||||
|
@ -83,16 +83,37 @@
|
||||||
<ol>
|
<ol>
|
||||||
{% for community in moderates %}
|
{% for community in moderates %}
|
||||||
<li>
|
<li>
|
||||||
<a href="/c/{{ community.link() }}">
|
<a href="/c/{{ community.link() }}"><img src="{{ community.icon_image() }}" class="community_icon rounded-circle" loading="lazy" />{{ community.display_name() }}</a>
|
||||||
<img src="{{ community.icon_image() }}" class="community_icon rounded-circle" loading="lazy" />
|
|
||||||
{{ community.display_name() }}
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if current_user.is_authenticated and (user_access('ban users', current_user.id) or user_access('manage users', current_user.id)) and user.id != current_user.id %}
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>{{ _('Crush') }}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
{% if user_access('ban users', current_user.id) %}
|
||||||
|
<div class="col-4">
|
||||||
|
<a class="w-100 btn btn-primary confirm_first" href="/u/{{ user.user_name }}/ban">{{ _('Ban') }}</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if user_access('manage users', current_user.id) %}
|
||||||
|
<div class="col-4">
|
||||||
|
<a class="w-100 btn btn-primary confirm_first" href="/u/{{ user.user_name }}/delete">{{ _('Delete') }}</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<a class="w-100 btn btn-primary confirm_first" href="/u/{{ user.user_name }}/ban_purge">{{ _('Ban + Purge') }}</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -6,14 +6,16 @@ from app import db
|
||||||
from app.models import Post, Community, CommunityMember, User, PostReply
|
from app.models import Post, Community, CommunityMember, User, PostReply
|
||||||
from app.user import bp
|
from app.user import bp
|
||||||
from app.user.forms import ProfileForm, SettingsForm
|
from app.user.forms import ProfileForm, SettingsForm
|
||||||
from app.utils import get_setting, render_template, markdown_to_html
|
from app.utils import get_setting, render_template, markdown_to_html, user_access
|
||||||
from sqlalchemy import desc, or_
|
from sqlalchemy import desc, or_
|
||||||
|
|
||||||
|
|
||||||
def show_profile(user):
|
def show_profile(user):
|
||||||
|
if user.deleted or user.banned and current_user.is_anonymous():
|
||||||
|
abort(404)
|
||||||
posts = Post.query.filter_by(user_id=user.id).order_by(desc(Post.posted_at)).all()
|
posts = Post.query.filter_by(user_id=user.id).order_by(desc(Post.posted_at)).all()
|
||||||
moderates = Community.query.filter_by(banned=False).join(CommunityMember).filter(or_(CommunityMember.is_moderator, CommunityMember.is_owner))
|
moderates = Community.query.filter_by(banned=False).join(CommunityMember).filter(or_(CommunityMember.is_moderator, CommunityMember.is_owner))
|
||||||
if user.id != current_user.id:
|
if current_user.is_anonymous or user.id != current_user.id:
|
||||||
moderates = moderates.filter(Community.private_mods == False)
|
moderates = moderates.filter(Community.private_mods == False)
|
||||||
post_replies = PostReply.query.filter_by(user_id=user.id).order_by(desc(PostReply.posted_at)).all()
|
post_replies = PostReply.query.filter_by(user_id=user.id).order_by(desc(PostReply.posted_at)).all()
|
||||||
canonical = user.ap_public_url if user.ap_public_url else None
|
canonical = user.ap_public_url if user.ap_public_url else None
|
||||||
|
@ -78,3 +80,80 @@ def change_settings(actor):
|
||||||
form.manually_approves_followers.data = current_user.ap_manually_approves_followers
|
form.manually_approves_followers.data = current_user.ap_manually_approves_followers
|
||||||
|
|
||||||
return render_template('user/edit_settings.html', title=_('Edit profile'), form=form, user=current_user)
|
return render_template('user/edit_settings.html', title=_('Edit profile'), form=form, user=current_user)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/u/<actor>/ban', methods=['GET'])
|
||||||
|
def ban_profile(actor):
|
||||||
|
if user_access('ban users', current_user.id):
|
||||||
|
actor = actor.strip()
|
||||||
|
user = User.query.filter_by(user_name=actor, deleted=False).first()
|
||||||
|
if user is None:
|
||||||
|
user = User.query.filter_by(ap_id=actor, deleted=False).first()
|
||||||
|
if user is None:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
if user.id == current_user.id:
|
||||||
|
flash('You cannot ban yourself.', 'error')
|
||||||
|
else:
|
||||||
|
user.banned = True
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flash(f'{actor} has been banned.')
|
||||||
|
else:
|
||||||
|
abort(401)
|
||||||
|
|
||||||
|
return redirect(f'/u/{actor}')
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/u/<actor>/delete', methods=['GET'])
|
||||||
|
def delete_profile(actor):
|
||||||
|
if user_access('manage users', current_user.id):
|
||||||
|
actor = actor.strip()
|
||||||
|
user = User.query.filter_by(user_name=actor, deleted=False).first()
|
||||||
|
if user is None:
|
||||||
|
user = User.query.filter_by(ap_id=actor, deleted=False).first()
|
||||||
|
if user is None:
|
||||||
|
abort(404)
|
||||||
|
if user.id == current_user.id:
|
||||||
|
flash('You cannot delete yourself.', 'error')
|
||||||
|
else:
|
||||||
|
user.banned = True
|
||||||
|
user.deleted = True
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flash(f'{actor} has been deleted.')
|
||||||
|
else:
|
||||||
|
abort(401)
|
||||||
|
|
||||||
|
return redirect(f'/u/{actor}')
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/u/<actor>/ban_purge', methods=['GET'])
|
||||||
|
def ban_purge_profile(actor):
|
||||||
|
if user_access('manage users', current_user.id):
|
||||||
|
actor = actor.strip()
|
||||||
|
user = User.query.filter_by(user_name=actor, deleted=False).first()
|
||||||
|
if user is None:
|
||||||
|
user = User.query.filter_by(ap_id=actor, deleted=False).first()
|
||||||
|
if user is None:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
if user.id == current_user.id:
|
||||||
|
flash('You cannot purge yourself.', 'error')
|
||||||
|
else:
|
||||||
|
user.banned = True
|
||||||
|
user.deleted = True
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
user.purge_content()
|
||||||
|
db.session.delete(user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# todo: empty relevant caches
|
||||||
|
# todo: federate deletion
|
||||||
|
|
||||||
|
flash(f'{actor} has been banned, deleted and all their content deleted.')
|
||||||
|
else:
|
||||||
|
abort(401)
|
||||||
|
|
||||||
|
return redirect(f'/u/{actor}')
|
||||||
|
|
19
app/utils.py
19
app/utils.py
|
@ -8,8 +8,11 @@ from bs4 import BeautifulSoup
|
||||||
import requests
|
import requests
|
||||||
import os
|
import os
|
||||||
from flask import current_app, json
|
from flask import current_app, json
|
||||||
|
from flask_login import current_user
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
from app import db, cache
|
from app import db, cache
|
||||||
from app.models import Settings, Domain, Instance, BannedInstances
|
from app.models import Settings, Domain, Instance, BannedInstances, User
|
||||||
|
|
||||||
|
|
||||||
# Flask's render_template function, with support for themes added
|
# Flask's render_template function, with support for themes added
|
||||||
|
@ -141,7 +144,10 @@ def html_to_markdown_worker(element, indent_level=0):
|
||||||
|
|
||||||
|
|
||||||
def markdown_to_html(markdown_text) -> str:
|
def markdown_to_html(markdown_text) -> str:
|
||||||
return allowlist_html(markdown2.markdown(markdown_text, safe_mode=True))
|
if markdown_text:
|
||||||
|
return allowlist_html(markdown2.markdown(markdown_text, safe_mode=True))
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
def domain_from_url(url: str) -> Domain:
|
def domain_from_url(url: str) -> Domain:
|
||||||
|
@ -167,3 +173,12 @@ def digits(input: int) -> int:
|
||||||
return 1 # Special case: 0 has 1 digit
|
return 1 # Special case: 0 has 1 digit
|
||||||
else:
|
else:
|
||||||
return math.floor(math.log10(abs(input))) + 1
|
return math.floor(math.log10(abs(input))) + 1
|
||||||
|
|
||||||
|
|
||||||
|
@cache.memoize(timeout=50)
|
||||||
|
def user_access(permission: str, user_id: int) -> bool:
|
||||||
|
has_access = db.session.execute(text('SELECT * FROM "role_permission" as rp ' +
|
||||||
|
'INNER JOIN user_role ur on rp.role_id = ur.role_id ' +
|
||||||
|
'WHERE ur.user_id = :user_id AND rp.permission = :permission'),
|
||||||
|
{'user_id': user_id, 'permission': permission}).first()
|
||||||
|
return has_access is not None
|
34
migrations/versions/c88bbba381b5_community_creator.py
Normal file
34
migrations/versions/c88bbba381b5_community_creator.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
"""community creator
|
||||||
|
|
||||||
|
Revision ID: c88bbba381b5
|
||||||
|
Revises: 882e33231c5b
|
||||||
|
Create Date: 2023-10-21 15:32:15.856895
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'c88bbba381b5'
|
||||||
|
down_revision = '882e33231c5b'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('community', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('user_id', sa.Integer(), nullable=True))
|
||||||
|
batch_op.create_foreign_key(None, 'user', ['user_id'], ['id'])
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('community', schema=None) as batch_op:
|
||||||
|
batch_op.drop_constraint(None, type_='foreignkey')
|
||||||
|
batch_op.drop_column('user_id')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
|
@ -6,7 +6,7 @@ from app import create_app, db, cli
|
||||||
import os, click
|
import os, click
|
||||||
from flask import session, g
|
from flask import session, g
|
||||||
from app.constants import POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_ARTICLE
|
from app.constants import POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_ARTICLE
|
||||||
from app.utils import getmtime, gibberish, shorten_string, shorten_url, digits
|
from app.utils import getmtime, gibberish, shorten_string, shorten_url, digits, user_access
|
||||||
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
cli.register(app)
|
cli.register(app)
|
||||||
|
@ -29,6 +29,7 @@ with app.app_context():
|
||||||
app.jinja_env.globals['len'] = len
|
app.jinja_env.globals['len'] = len
|
||||||
app.jinja_env.globals['digits'] = digits
|
app.jinja_env.globals['digits'] = digits
|
||||||
app.jinja_env.globals['str'] = str
|
app.jinja_env.globals['str'] = str
|
||||||
|
app.jinja_env.globals['user_access'] = user_access
|
||||||
app.jinja_env.filters['shorten'] = shorten_string
|
app.jinja_env.filters['shorten'] = shorten_string
|
||||||
app.jinja_env.filters['shorten_url'] = shorten_url
|
app.jinja_env.filters['shorten_url'] = shorten_url
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue