mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 11:26:56 -08:00
report posts and communities. also block domains and instances
This commit is contained in:
parent
ac8a229475
commit
46900390a5
17 changed files with 393 additions and 18 deletions
|
@ -3,7 +3,7 @@ from wtforms import StringField, SubmitField, TextAreaField, BooleanField, Hidde
|
|||
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, Length, Optional
|
||||
from flask_babel import _, lazy_gettext as _l
|
||||
|
||||
from app.utils import domain_from_url
|
||||
from app.utils import domain_from_url, MultiCheckboxField
|
||||
|
||||
|
||||
class AddLocalCommunity(FlaskForm):
|
||||
|
@ -83,3 +83,17 @@ class CreatePostForm(FlaskForm):
|
|||
|
||||
return True
|
||||
|
||||
|
||||
class ReportCommunityForm(FlaskForm):
|
||||
reason_choices = [('1', _l('Breaks instance rules')),
|
||||
('2', _l('Abandoned by moderators')),
|
||||
('3', _l('Cult')),
|
||||
('4', _l('Scam')),
|
||||
('5', _l('Alt-right pipeline')),
|
||||
('6', _l('Hate / genocide')),
|
||||
('7', _l('Other')),
|
||||
]
|
||||
reasons = MultiCheckboxField(_l('Reason'), choices=reason_choices)
|
||||
description = StringField(_l('More info'))
|
||||
report_remote = BooleanField('Also send report to originating instance')
|
||||
submit = SubmitField(_l('Report'))
|
||||
|
|
|
@ -1,24 +1,22 @@
|
|||
from flask import redirect, url_for, flash, request, make_response, session, Markup, current_app, abort
|
||||
from flask_login import login_user, logout_user, current_user, login_required
|
||||
from flask_babel import _
|
||||
from pillow_heif import register_heif_opener
|
||||
from sqlalchemy import or_, desc
|
||||
|
||||
from app import db, constants, cache
|
||||
from app.activitypub.signature import RsaKeys, HttpSignature
|
||||
from app.activitypub.util import default_context
|
||||
from app.community.forms import SearchRemoteCommunity, AddLocalCommunity, CreatePostForm
|
||||
from app.community.forms import SearchRemoteCommunity, AddLocalCommunity, CreatePostForm, ReportCommunityForm
|
||||
from app.community.util import search_for_community, community_url_exists, actor_to_community, \
|
||||
ensure_directory_exists, opengraph_parse, url_to_thumbnail_file, save_post, save_icon_file, save_banner_file
|
||||
from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE, \
|
||||
SUBSCRIPTION_PENDING
|
||||
from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post, \
|
||||
File, PostVote, utcnow
|
||||
File, PostVote, utcnow, Report, Notification, InstanceBlock
|
||||
from app.community import bp
|
||||
from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \
|
||||
shorten_string, markdown_to_text, domain_from_url, validate_image, gibberish, community_membership, ap_datetime, \
|
||||
request_etag_matches, return_304
|
||||
import os
|
||||
from feedgen.feed import FeedGenerator
|
||||
from datetime import timezone
|
||||
|
||||
|
@ -38,7 +36,7 @@ def add_local():
|
|||
community = Community(title=form.community_name.data, name=form.url.data, description=form.description.data,
|
||||
rules=form.rules.data, nsfw=form.nsfw.data, private_key=private_key,
|
||||
public_key=public_key,
|
||||
ap_profile_id=current_app.config['SERVER_NAME'] + '/c/' + form.url.data,
|
||||
ap_profile_id='https://' + current_app.config['SERVER_NAME'] + '/c/' + form.url.data,
|
||||
subscriptions_count=1)
|
||||
icon_file = request.files['icon_file']
|
||||
if icon_file and icon_file.filename != '':
|
||||
|
@ -377,4 +375,43 @@ def add_post(actor):
|
|||
images_disabled=images_disabled)
|
||||
|
||||
|
||||
@login_required
|
||||
@bp.route('/community/<int:community_id>/report', methods=['GET', 'POST'])
|
||||
def community_report(community_id: int):
|
||||
community = Community.query.get_or_404(community_id)
|
||||
form = ReportCommunityForm()
|
||||
if form.validate_on_submit():
|
||||
report = Report(reasons=form.reasons_to_string(form.reasons.data), description=form.description.data,
|
||||
type=1, reporter_id=current_user.id, suspect_community_id=community.id)
|
||||
db.session.add(report)
|
||||
|
||||
# Notify admin
|
||||
# todo: find all instance admin(s). for now just load User.id == 1
|
||||
admins = [User.query.get_or_404(1)]
|
||||
for admin in admins:
|
||||
notification = Notification(user_id=admin.id, title=_('A post has been reported'),
|
||||
url=community.local_url(),
|
||||
author_id=current_user.id)
|
||||
db.session.add(notification)
|
||||
db.session.commit()
|
||||
|
||||
# todo: federate report to originating instance
|
||||
if not community.is_local() and form.report_remote.data:
|
||||
...
|
||||
|
||||
flash(_('Community has been reported, thank you!'))
|
||||
return redirect(community.local_url())
|
||||
|
||||
return render_template('community/community_report.html', title=_('Report community'), form=form, community=community)
|
||||
|
||||
|
||||
@login_required
|
||||
@bp.route('/community/<int:community_id>/block_instance', methods=['GET', 'POST'])
|
||||
def community_block_instance(community_id: int):
|
||||
community = Community.query.get_or_404(community_id)
|
||||
existing = InstanceBlock.query.filter_by(user_id=current_user.id, instance_id=community.instance_id).first()
|
||||
if not existing:
|
||||
db.session.add(InstanceBlock(user_id=current_user.id, instance_id=community.instance_id))
|
||||
db.session.commit()
|
||||
flash(_('Content from %(name)s will be hidden.', name=community.instance.domain))
|
||||
return redirect(community.local_url())
|
||||
|
|
|
@ -70,11 +70,14 @@ class Community(db.Model):
|
|||
title = db.Column(db.String(256))
|
||||
description = db.Column(db.Text)
|
||||
rules = db.Column(db.Text)
|
||||
content_warning = db.Column(db.Text) # "Are you sure you want to view this community?"
|
||||
subscriptions_count = db.Column(db.Integer, default=0)
|
||||
post_count = db.Column(db.Integer, default=0)
|
||||
post_reply_count = db.Column(db.Integer, default=0)
|
||||
nsfw = db.Column(db.Boolean, default=False)
|
||||
nsfl = db.Column(db.Boolean, default=False)
|
||||
instance_id = db.Column(db.Integer, db.ForeignKey('instance.id'), index=True)
|
||||
low_quality = db.Column(db.Boolean, default=False) # upvotes earned in low quality communities don't improve reputation
|
||||
created_at = db.Column(db.DateTime, default=utcnow)
|
||||
last_active = db.Column(db.DateTime, default=utcnow)
|
||||
public_key = db.Column(db.Text)
|
||||
|
@ -94,6 +97,7 @@ class Community(db.Model):
|
|||
|
||||
banned = db.Column(db.Boolean, default=False)
|
||||
restricted_to_mods = db.Column(db.Boolean, default=False)
|
||||
new_mods_wanted = db.Column(db.Boolean, default=False)
|
||||
searchable = db.Column(db.Boolean, default=True)
|
||||
private_mods = db.Column(db.Boolean, default=False)
|
||||
|
||||
|
@ -175,6 +179,12 @@ class Community(db.Model):
|
|||
def is_local(self):
|
||||
return self.ap_id is None or self.profile_id().startswith('https://' + current_app.config['SERVER_NAME'])
|
||||
|
||||
def local_url(self):
|
||||
if self.is_local():
|
||||
return self.ap_profile_id
|
||||
else:
|
||||
return f"https://{current_app.config['SERVER_NAME']}/c/{self.ap_id}"
|
||||
|
||||
|
||||
user_role = db.Table('user_role',
|
||||
db.Column('user_id', db.Integer, db.ForeignKey('user.id')),
|
||||
|
@ -207,6 +217,7 @@ class User(UserMixin, db.Model):
|
|||
bounces = db.Column(db.SmallInteger, default=0)
|
||||
timezone = db.Column(db.String(20))
|
||||
reputation = db.Column(db.Float, default=0.0)
|
||||
attitude = db.Column(db.Float, default=1.0) # (upvotes cast - downvotes cast) / (upvotes + downvotes). A number between 1 and -1 is the ratio between up and down votes they cast
|
||||
stripe_customer_id = db.Column(db.String(50))
|
||||
stripe_subscription_id = db.Column(db.String(50))
|
||||
searchable = db.Column(db.Boolean, default=True)
|
||||
|
@ -410,6 +421,7 @@ class Post(db.Model):
|
|||
community_id = db.Column(db.Integer, db.ForeignKey('community.id'), index=True)
|
||||
image_id = db.Column(db.Integer, db.ForeignKey('file.id'), index=True)
|
||||
domain_id = db.Column(db.Integer, db.ForeignKey('domain.id'), index=True)
|
||||
instance_id = db.Column(db.Integer, db.ForeignKey('instance.id'), index=True)
|
||||
slug = db.Column(db.String(255))
|
||||
title = db.Column(db.String(255))
|
||||
url = db.Column(db.String(2048))
|
||||
|
@ -623,6 +635,15 @@ class Instance(db.Model):
|
|||
created_at = db.Column(db.DateTime, default=utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=utcnow)
|
||||
|
||||
posts = db.relationship('Post', backref='instance', lazy='dynamic')
|
||||
communities = db.relationship('Community', backref='instance', lazy='dynamic')
|
||||
|
||||
|
||||
class InstanceBlock(db.Model):
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
|
||||
instance_id = db.Column(db.Integer, db.ForeignKey('instance.id'), primary_key=True)
|
||||
created_at = db.Column(db.DateTime, default=utcnow)
|
||||
|
||||
|
||||
class Settings(db.Model):
|
||||
name = db.Column(db.String(50), primary_key=True)
|
||||
|
@ -716,6 +737,20 @@ class Notification(db.Model):
|
|||
created_at = db.Column(db.DateTime, default=utcnow)
|
||||
|
||||
|
||||
class Report(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
reasons = db.Column(db.String(256))
|
||||
description = db.Column(db.String(256))
|
||||
status = db.Column(db.Integer, default=0)
|
||||
type = db.Column(db.Integer, default=0) # 0 = user, 1 = post, 2 = reply, 3 = community
|
||||
reporter_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||
suspect_community_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||
suspect_user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||
suspect_post_id = db.Column(db.Integer, db.ForeignKey('post.id'))
|
||||
suspect_reply_id = db.Column(db.Integer, db.ForeignKey('post_reply.id'))
|
||||
created_at = db.Column(db.DateTime, default=utcnow)
|
||||
updated = db.Column(db.DateTime, default=utcnow)
|
||||
|
||||
@login.user_loader
|
||||
def load_user(id):
|
||||
return User.query.get(int(id))
|
||||
|
|
|
@ -1,10 +1,37 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import TextAreaField, SubmitField, BooleanField
|
||||
from wtforms import TextAreaField, SubmitField, BooleanField, StringField
|
||||
from wtforms.validators import DataRequired, Length
|
||||
from flask_babel import _, lazy_gettext as _l
|
||||
|
||||
from app.utils import MultiCheckboxField
|
||||
|
||||
|
||||
class NewReplyForm(FlaskForm):
|
||||
body = TextAreaField(_l('Body'), render_kw={'placeholder': 'What are your thoughts?', 'rows': 3}, validators={DataRequired(), Length(min=3, max=5000)})
|
||||
notify_author = BooleanField(_l('Notify about replies'))
|
||||
submit = SubmitField(_l('Comment'))
|
||||
submit = SubmitField(_l('Comment'))
|
||||
|
||||
|
||||
|
||||
class ReportPostForm(FlaskForm):
|
||||
reason_choices = [('1', _l('Breaks community rules')), ('7', _l('Spam')), ('2', _l('Harassment')),
|
||||
('3', _l('Threatening violence')), ('4', _l('Hate / genocide')),
|
||||
('6', _l('Sharing personal information')),
|
||||
('5', _l('Minor abuse or sexualization')),
|
||||
('8', _l('Non-consensual intimate media')),
|
||||
('9', _l('Prohibited transaction')), ('10', _l('Impersonation')),
|
||||
('11', _l('Copyright violation')), ('12', _l('Trademark violation')),
|
||||
('13', _l('Self-harm or suicide')),
|
||||
('14', _l('Other'))]
|
||||
reasons = MultiCheckboxField(_l('Reason'), choices=reason_choices)
|
||||
description = StringField(_l('More info'))
|
||||
report_remote = BooleanField('Also send report to originating instance')
|
||||
submit = SubmitField(_l('Report'))
|
||||
|
||||
def reasons_to_string(self, reason_data) -> str:
|
||||
result = []
|
||||
for reason_id in reason_data:
|
||||
for choice in self.reason_choices:
|
||||
if choice[0] == reason_id:
|
||||
result.append(str(choice[1]))
|
||||
return ', '.join(result)
|
||||
|
|
|
@ -9,12 +9,12 @@ from app import db, constants
|
|||
from app.activitypub.signature import HttpSignature
|
||||
from app.activitypub.util import default_context
|
||||
from app.community.util import save_post
|
||||
from app.post.forms import NewReplyForm
|
||||
from app.post.forms import NewReplyForm, ReportPostForm
|
||||
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, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE
|
||||
from app.models import Post, PostReply, \
|
||||
PostReplyVote, PostVote, Notification, utcnow
|
||||
PostReplyVote, PostVote, Notification, utcnow, UserBlock, DomainBlock, InstanceBlock, Report
|
||||
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, domain_from_url, validate_image, gibberish, ap_datetime, return_304, \
|
||||
|
@ -407,12 +407,73 @@ def post_delete(post_id: int):
|
|||
post.flush_cache()
|
||||
db.session.delete(post)
|
||||
db.session.commit()
|
||||
flash('Post deleted.')
|
||||
flash(_('Post deleted.'))
|
||||
return redirect(url_for('activitypub.community_profile', actor=community.ap_id if community.ap_id is not None else community.name))
|
||||
|
||||
|
||||
@login_required
|
||||
@bp.route('/post/<int:post_id>/report', methods=['GET', 'POST'])
|
||||
def post_report(post_id: int):
|
||||
...
|
||||
post = Post.query.get_or_404(post_id)
|
||||
form = ReportPostForm()
|
||||
if form.validate_on_submit():
|
||||
report = Report(reasons=form.reasons_to_string(form.reasons.data), description=form.description.data,
|
||||
type=1, reporter_id=current_user.id, suspect_post_id=post.id)
|
||||
db.session.add(report)
|
||||
|
||||
# Notify moderators
|
||||
for mod in post.community.moderators():
|
||||
notification = Notification(user_id=mod.user_id, title=_('A post has been reported'),
|
||||
url=f"https://{current_app.config['SERVER_NAME']}/post/{post.id}",
|
||||
author_id=current_user.id)
|
||||
db.session.add(notification)
|
||||
# todo: Also notify admins for certain types of report
|
||||
db.session.commit()
|
||||
|
||||
# todo: federate report to originating instance
|
||||
if not post.community.is_local() and form.report_remote.data:
|
||||
...
|
||||
|
||||
flash(_('Post has been reported, thank you!'))
|
||||
return redirect(post.community.local_url())
|
||||
|
||||
return render_template('post/post_report.html', title=_('Report post'), form=form, post=post)
|
||||
|
||||
|
||||
@login_required
|
||||
@bp.route('/post/<int:post_id>/block_user', methods=['GET', 'POST'])
|
||||
def post_block_user(post_id: int):
|
||||
post = Post.query.get_or_404(post_id)
|
||||
existing = UserBlock.query.filter_by(blocker_id=current_user.id, blocked_id=post.author.id).first()
|
||||
if not existing:
|
||||
db.session.add(UserBlock(blocker_id=current_user.id, blocked_id=post.author.id))
|
||||
db.session.commit()
|
||||
flash(_('%(name)s has been blocked.', name=post.author.user_name))
|
||||
|
||||
# todo: federate block to post author instance
|
||||
|
||||
return redirect(post.community.local_url())
|
||||
|
||||
|
||||
@login_required
|
||||
@bp.route('/post/<int:post_id>/block_domain', methods=['GET', 'POST'])
|
||||
def post_block_domain(post_id: int):
|
||||
post = Post.query.get_or_404(post_id)
|
||||
existing = DomainBlock.query.filter_by(user_id=current_user.id, domain_id=post.domain_id).first()
|
||||
if not existing:
|
||||
db.session.add(DomainBlock(user_id=current_user.id, domain_id=post.domain_id))
|
||||
db.session.commit()
|
||||
flash(_('Posts linking to %(name)s will be hidden.', name=post.domain.name))
|
||||
return redirect(post.community.local_url())
|
||||
|
||||
|
||||
@login_required
|
||||
@bp.route('/post/<int:post_id>/block_instance', methods=['GET', 'POST'])
|
||||
def post_block_instance(post_id: int):
|
||||
post = Post.query.get_or_404(post_id)
|
||||
existing = InstanceBlock.query.filter_by(user_id=current_user.id, instance_id=post.instance_id).first()
|
||||
if not existing:
|
||||
db.session.add(InstanceBlock(user_id=current_user.id, instance_id=post.instance_id))
|
||||
db.session.commit()
|
||||
flash(_('Content from %(name)s will be hidden.', name=post.instance.domain))
|
||||
return redirect(post.community.local_url())
|
||||
|
|
|
@ -178,6 +178,10 @@
|
|||
content: "\ea03";
|
||||
}
|
||||
|
||||
.fe-block::before {
|
||||
content: "\ea04";
|
||||
}
|
||||
|
||||
.fe-report::before {
|
||||
content: "\e967";
|
||||
}
|
||||
|
|
|
@ -181,6 +181,10 @@ nav, etc which are used site-wide */
|
|||
content: "\ea03";
|
||||
}
|
||||
|
||||
.fe-block::before {
|
||||
content: "\ea04";
|
||||
}
|
||||
|
||||
.fe-report::before {
|
||||
content: "\e967";
|
||||
}
|
||||
|
@ -579,6 +583,14 @@ fieldset legend {
|
|||
padding-left: 3px;
|
||||
}
|
||||
|
||||
#reasons {
|
||||
border: none;
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
height: 135px;
|
||||
}
|
||||
|
||||
.table tr th {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
|
|
@ -325,6 +325,14 @@ nav, etc which are used site-wide */
|
|||
}
|
||||
}
|
||||
|
||||
#reasons {
|
||||
border: none;
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
height: 135px;
|
||||
}
|
||||
|
||||
.table {
|
||||
tr th {
|
||||
vertical-align: middle;
|
||||
|
|
|
@ -180,6 +180,10 @@
|
|||
content: "\ea03";
|
||||
}
|
||||
|
||||
.fe-block::before {
|
||||
content: "\ea04";
|
||||
}
|
||||
|
||||
.fe-report::before {
|
||||
content: "\e967";
|
||||
}
|
||||
|
@ -480,6 +484,15 @@ nav.navbar {
|
|||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.post_options_link {
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: -2px;
|
||||
width: 41px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #777;
|
||||
|
|
|
@ -197,6 +197,14 @@ nav.navbar {
|
|||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.post_options_link {
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: -2px;
|
||||
width: 41px;
|
||||
text-decoration: none;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: $dark-grey;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div class="row">
|
||||
<div class="row position-relative">
|
||||
{% if post.type == POST_TYPE_IMAGE %}
|
||||
<div class="col post_type_image">
|
||||
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
|
||||
|
@ -72,6 +72,7 @@
|
|||
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('post.post_options', post_id=post.id) }}" class="post_options_link" rel="nofollow"><span class="fe fe-options" title="Options"> </span></a>
|
||||
</div>
|
||||
|
||||
{% if post.body_html %}
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
<a href="{{ url_for('activitypub.post_ap', post_id=post.id, _anchor='replies') }}"><span class="fe fe-reply"></span></a>
|
||||
<a href="{{ url_for('activitypub.post_ap', post_id=post.id, _anchor='replies') }}">{{ post.reply_count }}</a>
|
||||
</div>
|
||||
<div class="col-2"><a href="{{ url_for('post.post_options', post_id=post.id) }}"><span class="fe fe-options" title="Options"> </span></a></div>
|
||||
<div class="col-2"><a href="{{ url_for('post.post_options', post_id=post.id) }}" rel="nofollow"><span class="fe fe-options" title="Options"> </span></a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-2">
|
||||
|
|
|
@ -9,11 +9,25 @@
|
|||
<div class="card-title">{{ _('Options for "%(post_title)s"', post_title=post.title) }}</div>
|
||||
<ul class="option_list">
|
||||
{% if current_user.is_authenticated and (post.user_id == current_user.id or post.community.is_moderator()) %}
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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 %}
|
||||
<li><a href="{{ url_for('post.post_report', post_id=post.id) }}" class="no-underline"><span class="fe fe-report"></span> Report</a></li>
|
||||
<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.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 %}
|
||||
<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 %(name)s', name=post.instance.domain) }}</a></li>
|
||||
{% endif %}
|
||||
<li><a href="{{ url_for('post.post_report', post_id=post.id) }}" class="no-underline"><span class="fe fe-report"></span>
|
||||
{{ _('Report to moderators') }}</a></li>
|
||||
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
17
app/templates/post/post_report.html
Normal file
17
app/templates/post/post_report.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
{% extends "base.html" %}
|
||||
{% 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">{{ _('Report "%(post_title)s"', post_title=post.title) }}</div>
|
||||
<div class="card-body">
|
||||
{{ render_form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -14,7 +14,8 @@ import imghdr
|
|||
from flask import current_app, json, redirect, url_for, request, make_response, Response
|
||||
from flask_login import current_user
|
||||
from sqlalchemy import text
|
||||
|
||||
from wtforms.fields import SelectField, SelectMultipleField
|
||||
from wtforms.widgets import Select, html_params, ListWidget, CheckboxInput
|
||||
from app import db, cache
|
||||
from app.models import Settings, Domain, Instance, BannedInstances, User, Community
|
||||
|
||||
|
@ -293,3 +294,8 @@ def back(default_url):
|
|||
# format a datetime in a way that is used in ActivityPub
|
||||
def ap_datetime(date_time: datetime) -> str:
|
||||
return date_time.isoformat() + '+00:00'
|
||||
|
||||
|
||||
class MultiCheckboxField(SelectMultipleField):
|
||||
widget = ListWidget(prefix_label=False)
|
||||
option_widget = CheckboxInput()
|
82
migrations/versions/31dfc1d1d3f6_report_block.py
Normal file
82
migrations/versions/31dfc1d1d3f6_report_block.py
Normal file
|
@ -0,0 +1,82 @@
|
|||
"""report_block
|
||||
|
||||
Revision ID: 31dfc1d1d3f6
|
||||
Revises: b36dac7696d1
|
||||
Create Date: 2023-12-13 19:11:27.447598
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '31dfc1d1d3f6'
|
||||
down_revision = 'b36dac7696d1'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('instance_block',
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('instance_id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['instance_id'], ['instance.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('user_id', 'instance_id')
|
||||
)
|
||||
op.create_table('report',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('reasons', sa.String(length=256), nullable=True),
|
||||
sa.Column('description', sa.String(length=256), nullable=True),
|
||||
sa.Column('status', sa.Integer(), nullable=True),
|
||||
sa.Column('type', sa.Integer(), nullable=True),
|
||||
sa.Column('reporter_id', sa.Integer(), nullable=True),
|
||||
sa.Column('suspect_community_id', sa.Integer(), nullable=True),
|
||||
sa.Column('suspect_user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('suspect_post_id', sa.Integer(), nullable=True),
|
||||
sa.Column('suspect_reply_id', sa.Integer(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['reporter_id'], ['user.id'], ),
|
||||
sa.ForeignKeyConstraint(['suspect_community_id'], ['user.id'], ),
|
||||
sa.ForeignKeyConstraint(['suspect_post_id'], ['post.id'], ),
|
||||
sa.ForeignKeyConstraint(['suspect_reply_id'], ['post_reply.id'], ),
|
||||
sa.ForeignKeyConstraint(['suspect_user_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
with op.batch_alter_table('community', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('content_warning', sa.Text(), nullable=True))
|
||||
batch_op.add_column(sa.Column('low_quality', sa.Boolean(), nullable=True))
|
||||
batch_op.add_column(sa.Column('new_mods_wanted', sa.Boolean(), nullable=True))
|
||||
|
||||
with op.batch_alter_table('post', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('instance_id', sa.Integer(), nullable=True))
|
||||
batch_op.create_index(batch_op.f('ix_post_instance_id'), ['instance_id'], unique=False)
|
||||
batch_op.create_foreign_key(None, 'instance', ['instance_id'], ['id'])
|
||||
|
||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('attitude', sa.Float(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||
batch_op.drop_column('attitude')
|
||||
|
||||
with op.batch_alter_table('post', schema=None) as batch_op:
|
||||
batch_op.drop_constraint(None, type_='foreignkey')
|
||||
batch_op.drop_index(batch_op.f('ix_post_instance_id'))
|
||||
batch_op.drop_column('instance_id')
|
||||
|
||||
with op.batch_alter_table('community', schema=None) as batch_op:
|
||||
batch_op.drop_column('new_mods_wanted')
|
||||
batch_op.drop_column('low_quality')
|
||||
batch_op.drop_column('content_warning')
|
||||
|
||||
op.drop_table('report')
|
||||
op.drop_table('instance_block')
|
||||
# ### end Alembic commands ###
|
36
migrations/versions/5fb8f21295da_community_instance.py
Normal file
36
migrations/versions/5fb8f21295da_community_instance.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
"""community instance
|
||||
|
||||
Revision ID: 5fb8f21295da
|
||||
Revises: 31dfc1d1d3f6
|
||||
Create Date: 2023-12-13 20:57:09.647260
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '5fb8f21295da'
|
||||
down_revision = '31dfc1d1d3f6'
|
||||
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('instance_id', sa.Integer(), nullable=True))
|
||||
batch_op.create_index(batch_op.f('ix_community_instance_id'), ['instance_id'], unique=False)
|
||||
batch_op.create_foreign_key(None, 'instance', ['instance_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_index(batch_op.f('ix_community_instance_id'))
|
||||
batch_op.drop_column('instance_id')
|
||||
|
||||
# ### end Alembic commands ###
|
Loading…
Reference in a new issue