mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 19:36:56 -08:00
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
commit
fcde6e9d26
9 changed files with 238 additions and 16 deletions
|
@ -39,7 +39,7 @@ import arrow
|
|||
from cryptography.exceptions import InvalidSignature
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import padding, rsa
|
||||
from flask import Request, current_app
|
||||
from flask import Request, current_app, g
|
||||
from datetime import datetime
|
||||
from dateutil import parser
|
||||
from pyld import jsonld
|
||||
|
@ -81,8 +81,9 @@ def post_request(uri: str, body: dict | None, private_key: str, key_id: str, con
|
|||
if '@context' not in body: # add a default json-ld context if necessary
|
||||
body['@context'] = default_context()
|
||||
type = body['type'] if 'type' in body else ''
|
||||
log = ActivityPubLog(direction='out', activity_json=json.dumps(body), activity_type=type,
|
||||
result='processing', activity_id=body['id'], exception_message='')
|
||||
log = ActivityPubLog(direction='out', activity_type=type, result='processing', activity_id=body['id'], exception_message='')
|
||||
if g.site.log_activitypub_json:
|
||||
log.activity_json=json.dumps(body)
|
||||
db.session.add(log)
|
||||
db.session.commit()
|
||||
try:
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
from flask import request, g
|
||||
from flask_login import current_user
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SubmitField, TextAreaField, BooleanField, HiddenField, SelectField, FileField
|
||||
from validators import Min
|
||||
from wtforms import StringField, SubmitField, TextAreaField, BooleanField, HiddenField, SelectField, FileField, \
|
||||
DateField
|
||||
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, Length, Optional
|
||||
from flask_babel import _, lazy_gettext as _l
|
||||
|
||||
from app import db
|
||||
from app.models import Community
|
||||
from app.models import Community, utcnow
|
||||
from app.utils import domain_from_url, MultiCheckboxField
|
||||
from PIL import Image, ImageOps
|
||||
from io import BytesIO
|
||||
|
@ -65,6 +67,14 @@ class SearchRemoteCommunity(FlaskForm):
|
|||
submit = SubmitField(_l('Search'))
|
||||
|
||||
|
||||
class BanUserCommunityForm(FlaskForm):
|
||||
reason = StringField(_l('Reason'), render_kw={'autofocus': True}, validators=[DataRequired()])
|
||||
ban_until = DateField(_l('Ban until'))
|
||||
delete_posts = BooleanField(_l('Also delete all their posts'))
|
||||
delete_post_replies = BooleanField(_l('Also delete all their comments'))
|
||||
submit = SubmitField(_l('Ban'))
|
||||
|
||||
|
||||
class CreatePostForm(FlaskForm):
|
||||
communities = SelectField(_l('Community'), validators=[DataRequired()], coerce=int)
|
||||
post_type = HiddenField() # https://getbootstrap.com/docs/4.6/components/navs/#tabs
|
||||
|
|
|
@ -12,14 +12,15 @@ from app.activitypub.signature import RsaKeys, post_request
|
|||
from app.activitypub.util import default_context, notify_about_post, find_actor_or_create
|
||||
from app.chat.util import send_message
|
||||
from app.community.forms import SearchRemoteCommunity, CreatePostForm, ReportCommunityForm, \
|
||||
DeleteCommunityForm, AddCommunityForm, EditCommunityForm, AddModeratorForm
|
||||
DeleteCommunityForm, AddCommunityForm, EditCommunityForm, AddModeratorForm, BanUserCommunityForm
|
||||
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, send_to_remote_instance
|
||||
opengraph_parse, url_to_thumbnail_file, save_post, save_icon_file, save_banner_file, send_to_remote_instance, \
|
||||
delete_post_from_community, delete_post_reply_from_community
|
||||
from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE, \
|
||||
SUBSCRIPTION_PENDING, SUBSCRIPTION_MODERATOR
|
||||
from app.inoculation import inoculation
|
||||
from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post, \
|
||||
File, PostVote, utcnow, Report, Notification, InstanceBlock, ActivityPubLog, Topic, Conversation
|
||||
File, PostVote, utcnow, Report, Notification, InstanceBlock, ActivityPubLog, Topic, Conversation, PostReply
|
||||
from app.community import bp
|
||||
from app.user.utils import search_for_user
|
||||
from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \
|
||||
|
@ -772,6 +773,62 @@ def community_block_instance(community_id: int):
|
|||
return redirect(community.local_url())
|
||||
|
||||
|
||||
@bp.route('/community/<int:community_id>/<int:user_id>/ban_user_community', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def community_ban_user(community_id: int, user_id: int):
|
||||
community = Community.query.get_or_404(community_id)
|
||||
user = User.query.get_or_404(user_id)
|
||||
existing = CommunityBan.query.filter_by(community_id=community.id, user_id=user.id).first()
|
||||
|
||||
form = BanUserCommunityForm()
|
||||
if form.validate_on_submit():
|
||||
if not existing:
|
||||
new_ban = CommunityBan(community_id=community_id, user_id=user.id, banned_by=current_user.id,
|
||||
reason=form.reason.data)
|
||||
if form.ban_until.data is not None and form.ban_until.data < utcnow().date():
|
||||
new_ban.ban_until = form.ban_until.data
|
||||
db.session.add(new_ban)
|
||||
db.session.commit()
|
||||
flash(_('%(name)s has been banned.', name=user.display_name()))
|
||||
|
||||
if form.delete_posts.data:
|
||||
posts = Post.query.filter(Post.user_id == user.id, Post.community_id == community.id).all()
|
||||
for post in posts:
|
||||
delete_post_from_community(post.id)
|
||||
if posts:
|
||||
flash(_('Posts by %(name)s have been deleted.', name=user.display_name()))
|
||||
if form.delete_post_replies.data:
|
||||
post_replies = PostReply.query.filter(PostReply.user_id == user.id, Post.community_id == community.id).all()
|
||||
for post_reply in post_replies:
|
||||
delete_post_reply_from_community(post_reply.id)
|
||||
if post_replies:
|
||||
flash(_('Comments by %(name)s have been deleted.', name=user.display_name()))
|
||||
|
||||
# todo: federate ban to post author instance
|
||||
|
||||
# notify banned person
|
||||
if user.is_local():
|
||||
notify = Notification(title=shorten_string('You have been banned from ' + community.title),
|
||||
url=f'/', user_id=user.id,
|
||||
author_id=1)
|
||||
db.session.add(notify)
|
||||
user.unread_notifications += 1
|
||||
db.session.commit()
|
||||
else:
|
||||
...
|
||||
# todo: send chatmessage to remote user and federate it
|
||||
|
||||
return redirect(community.local_url())
|
||||
else:
|
||||
return render_template('community/community_ban_user.html', title=_('Ban from community'), form=form, community=community,
|
||||
user=user,
|
||||
moderating_communities=moderating_communities(current_user.get_id()),
|
||||
joined_communities=joined_communities(current_user.get_id()),
|
||||
inoculation=inoculation[randint(0, len(inoculation) - 1)]
|
||||
)
|
||||
|
||||
|
||||
|
||||
@bp.route('/<int:community_id>/notification', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def community_notification(community_id: int):
|
||||
|
|
|
@ -10,12 +10,13 @@ from pillow_heif import register_heif_opener
|
|||
|
||||
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, default_context
|
||||
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, \
|
||||
Instance, Notification, User
|
||||
from app.utils import get_request, gibberish, markdown_to_html, domain_from_url, allowlist_html, \
|
||||
html_to_markdown, is_image_url, ensure_directory_exists, inbox_domain, post_ranking, shorten_string, parse_page, remove_tracking_from_link
|
||||
html_to_markdown, is_image_url, ensure_directory_exists, inbox_domain, post_ranking, shorten_string, parse_page, \
|
||||
remove_tracking_from_link, ap_datetime, instance_banned
|
||||
from sqlalchemy import func
|
||||
import os
|
||||
|
||||
|
@ -283,6 +284,121 @@ def save_post(form, post: Post):
|
|||
g.site.last_active = utcnow()
|
||||
|
||||
|
||||
def delete_post_from_community(post_id):
|
||||
if current_app.debug:
|
||||
delete_post_from_community_task(post_id)
|
||||
else:
|
||||
delete_post_from_community_task.delay(post_id)
|
||||
|
||||
|
||||
@celery.task
|
||||
def delete_post_from_community_task(post_id):
|
||||
post = Post.query.get(post_id)
|
||||
community = post.community
|
||||
post.delete_dependencies()
|
||||
post.flush_cache()
|
||||
db.session.delete(post)
|
||||
db.session.commit()
|
||||
|
||||
if not community.local_only:
|
||||
delete_json = {
|
||||
'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,
|
||||
}
|
||||
|
||||
if not post.community.is_local(): # this is a remote community, send it to the instance that hosts it
|
||||
success = post_request(post.community.ap_inbox_url, delete_json, current_user.private_key,
|
||||
current_user.ap_profile_id + '#main-key')
|
||||
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': 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)
|
||||
|
||||
|
||||
def delete_post_reply_from_community(post_reply_id):
|
||||
if current_app.debug:
|
||||
delete_post_reply_from_community_task(post_reply_id)
|
||||
else:
|
||||
delete_post_reply_from_community_task.delay(post_reply_id)
|
||||
|
||||
|
||||
@celery.task
|
||||
def delete_post_reply_from_community_task(post_reply_id):
|
||||
post_reply = PostReply.query.get(post_reply_id)
|
||||
post = post_reply.post
|
||||
community = post.community
|
||||
if post_reply.user_id == current_user.id or community.is_moderator():
|
||||
if post_reply.has_replies():
|
||||
post_reply.body = 'Deleted by author' if post_reply.author.id == current_user.id else 'Deleted by moderator'
|
||||
post_reply.body_html = markdown_to_html(post_reply.body)
|
||||
else:
|
||||
post_reply.delete_dependencies()
|
||||
db.session.delete(post_reply)
|
||||
db.session.commit()
|
||||
post.flush_cache()
|
||||
# federate delete
|
||||
if not post.community.local_only:
|
||||
delete_json = {
|
||||
'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_reply.ap_id,
|
||||
}
|
||||
|
||||
if not post.community.is_local(): # this is a remote community, send it to the instance that hosts it
|
||||
success = post_request(post.community.ap_inbox_url, delete_json, current_user.private_key,
|
||||
current_user.ap_profile_id + '#main-key')
|
||||
|
||||
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': 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)
|
||||
|
||||
|
||||
def remove_old_file(file_id):
|
||||
remove_file = File.query.get(file_id)
|
||||
remove_file.delete_from_disk()
|
||||
|
|
|
@ -374,7 +374,12 @@ class Community(db.Model):
|
|||
|
||||
def user_is_banned(self, user):
|
||||
membership = CommunityMember.query.filter(CommunityMember.community_id == self.id, CommunityMember.user_id == user.id).first()
|
||||
return membership.is_banned if membership else False
|
||||
if membership.is_banned:
|
||||
return True
|
||||
banned = CommunityBan.query.filter(CommunityBan.community_id == self.id, CommunityBan.user_id == user.id).first()
|
||||
if banned:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def profile_id(self):
|
||||
|
@ -980,7 +985,7 @@ class CommunityMember(db.Model):
|
|||
|
||||
# people banned from communities
|
||||
class CommunityBan(db.Model):
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) # person who is banned, not the banner
|
||||
community_id = db.Column(db.Integer, db.ForeignKey('community.id'), primary_key=True)
|
||||
banned_by = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||
reason = db.Column(db.String(50))
|
||||
|
|
|
@ -17,7 +17,8 @@ from app.community.forms import CreatePostForm
|
|||
from app.post.util import post_replies, get_comment_branch, post_reply_count
|
||||
from app.constants import SUBSCRIPTION_MEMBER, POST_TYPE_LINK, POST_TYPE_IMAGE
|
||||
from app.models import Post, PostReply, \
|
||||
PostReplyVote, PostVote, Notification, utcnow, UserBlock, DomainBlock, InstanceBlock, Report, Site, Community, Topic
|
||||
PostReplyVote, PostVote, Notification, utcnow, UserBlock, DomainBlock, InstanceBlock, Report, Site, Community, \
|
||||
Topic
|
||||
from app.post import bp
|
||||
from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \
|
||||
shorten_string, markdown_to_text, gibberish, ap_datetime, return_304, \
|
||||
|
@ -585,7 +586,8 @@ 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)
|
||||
return render_template('post/post_options.html', post=post, moderating_communities=moderating_communities(current_user.get_id()),
|
||||
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()))
|
||||
|
||||
|
||||
|
|
|
@ -38,7 +38,13 @@
|
|||
<td><span style="color: red;">{{ activity.result }}</span></td>
|
||||
{% endif %}
|
||||
<td>{{ activity.exception_message if activity.exception_message else '' }}</td>
|
||||
<td><a href="{{ url_for('admin.activity_json', activity_id=activity.id) }}">View</a></td>
|
||||
<td>
|
||||
{% if activity.activity_json is none %}
|
||||
None
|
||||
{% else %}
|
||||
<a href="{{ url_for('admin.activity_json', activity_id=activity.id) }}">View</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
|
21
app/templates/community/community_ban_user.html
Normal file
21
app/templates/community/community_ban_user.html
Normal file
|
@ -0,0 +1,21 @@
|
|||
{% 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 %}
|
||||
<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">{{ _('Ban "%(user_name)s" from %(community_name)s', user_name=user.display_name(), community_name=community.title) }}</div>
|
||||
<div class="card-body">
|
||||
{{ render_form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -26,6 +26,10 @@
|
|||
{% 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() %}
|
||||
<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 %}
|
||||
<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>
|
||||
|
|
Loading…
Add table
Reference in a new issue