Add reporting system

Fixes #12
This commit is contained in:
rubenwardy 2022-01-20 23:30:56 +00:00
parent a47e6e8998
commit 7f5656df08
15 changed files with 155 additions and 24 deletions

@ -0,0 +1,66 @@
# ContentDB
# Copyright (C) 2022 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from flask import Blueprint, request, render_template, url_for
from flask_babel import lazy_gettext
from flask_login import current_user
from flask_wtf import FlaskForm
from werkzeug.utils import redirect
from wtforms import TextAreaField, SubmitField, BooleanField
from wtforms.fields.html5 import URLField
from wtforms.validators import InputRequired, Optional, Length
from app.models import User, UserRank
from app.tasks.emails import send_user_email
from app.tasks.webhooktasks import post_discord_webhook
from app.utils import isYes, isNo
bp = Blueprint("report", __name__)
class ReportForm(FlaskForm):
url = URLField(lazy_gettext("URL"), [Optional()])
message = TextAreaField(lazy_gettext("Message"), [InputRequired(), Length(10, 10000)])
submit = SubmitField(lazy_gettext("Report"))
@bp.route("/report/", methods=["GET", "POST"])
def report():
is_anon = not current_user.is_authenticated or not isNo(request.args.get("anon"))
form = ReportForm(formdata=request.form)
if request.method == "GET":
if "url" in request.args:
form.url.data = request.args["url"]
if form.validate_on_submit():
if current_user.is_authenticated:
user_info = f"{current_user.username}"
else:
user_info = request.headers.get("X-Forwarded-For") or request.remote_addr
url = request.args.get("url") or form.url.data or "?"
text = f"{url}\n\n{form.message.data}"
task = None
for admin in User.query.filter_by(rank=UserRank.ADMIN).all():
task = send_user_email.delay(admin.email, f"User report from {user_info}", text)
post_discord_webhook.delay(None if is_anon else current_user.username, f"**New Report**\n`{url}`\n\n{form.message.data}", True)
return redirect(url_for("tasks.check", id=task.id, r=url_for("homepage.home")))
return render_template("report/index.html", form=form, url=request.args.get("url"), is_anon=is_anon)

@ -9,7 +9,7 @@ toc: False
* [Non-free Licenses](non_free)
* [Why WTFPL is a terrible license](wtfpl)
* [Ranks and Permissions](ranks_permissions)
* [Reporting Content](reporting)
* [Contact Us](contact_us)
* [Top Packages Algorithm](top_packages)
* [Featured Packages](featured)

@ -0,0 +1,14 @@
title: Contact Us
## Reports
Please let us know if anything on the ContentDB violates our rules or any applicable
laws.
We take copyright violation and other offenses very seriously.
<a href="/report/" class="btn btn-primary">Report</a>
## Other
<a href="https://rubenwardy.com/contact/" class="btn btn-primary">Contact the admin</a>

@ -1,8 +0,0 @@
title: Reporting Content
Please let us know if anything on the ContentDB violates our rules or any applicable
laws.
We take copyright violation and other offenses very seriously.
<a href="https://rubenwardy.com/contact/" class="btn btn-success">Contact</a>

@ -27,7 +27,7 @@ including ones not covered by this document, and to ban users who abuse this ser
### 2.1. Acceptable Content
Sexually-orientated content is not permitted.
If in doubt at what this means, [contact us by raising a report](/help/reporting/).
If in doubt at what this means, [contact us by raising a report](/report/).
Mature content is permitted providing that it is labelled correctly.
See [Content Flags](/help/content_flags/).
@ -153,4 +153,4 @@ Doing so may result in temporary or permanent suspension from ContentDB.
## 7. Reporting Violations
See the [Reporting Content](/help/reporting/) page.
Please click "Report" on the package page.

@ -72,7 +72,7 @@ requested. See below.
## Removal Requests
Please [raise a report](https://content.minetest.net/help/reporting/) if you
Please [raise a report](https://content.minetest.net/report/?anon=0) if you
wish to remove your personal information.
ContentDB keeps a record of each username and forum topic on the forums,

@ -124,6 +124,9 @@ class ThreadReply(db.Model):
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
def get_url(self):
return url_for('threads.view', id=self.thread.id) + "#reply-" + str(self.id)
def checkPerm(self, user, perm):
if not user.is_authenticated:
return False

@ -15,7 +15,7 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from flask import render_template
from flask import render_template, escape
from flask_mail import Message
from app import mail
from app.models import Notification, db, EmailSubscription, User
@ -86,7 +86,7 @@ def send_email_with_reason(email, subject, text, html, reason):
msg = Message(subject, recipients=[email])
msg.body = text
html = html or text
html = html or f"<pre>{escape(text)}</pre>"
msg.html = render_template("emails/base.html", subject=subject, content=html, reason=reason, sub=sub)
mail.send(msg)

@ -1,6 +1,6 @@
from . import app, utils
from .models import Permission, Package, PackageState, PackageRelease
from .utils import abs_url_for, url_set_query, url_set_anchor
from .utils import abs_url_for, url_set_query, url_set_anchor, url_current
from flask_login import current_user
from flask_babel import format_timedelta, gettext
from urllib.parse import urlparse
@ -17,7 +17,7 @@ def inject_debug():
def inject_functions():
check_global_perm = Permission.checkPerm
return dict(abs_url_for=abs_url_for, url_set_query=url_set_query, url_set_anchor=url_set_anchor,
check_global_perm=check_global_perm, get_headings=get_headings)
check_global_perm=check_global_perm, get_headings=get_headings, url_current=url_current)
@app.context_processor
def inject_todo():

@ -234,7 +234,7 @@
<li class="list-inline-item"><a href="{{ url_for('flatpage', path='policy_and_guidance') }}">{{ _("Policy and Guidance") }}</a></li>
<li class="list-inline-item"><a href="{{ url_for('flatpage', path='help/api') }}">{{ _("API") }}</a></li>
<li class="list-inline-item"><a href="{{ url_for('flatpage', path='privacy_policy') }}">{{ _("Privacy Policy") }}</a></li>
<li class="list-inline-item"><a href="{{ url_for('flatpage', path='help/reporting') }}">{{ _("Report / DMCA") }}</a></li>
<li class="list-inline-item"><a href="{{ url_for('report.report', url=url_current(True)) }}">{{ _("Report") }}</a></li>
<li class="list-inline-item"><a href="https://monitor.rubenwardy.com/d/3ELzFy3Wz/contentdb">{{ _("Stats / Monitoring") }}</a></li>
<li class="list-inline-item"><a href="{{ url_for('users.list_all') }}">{{ _("User List") }}</a></li>
<li class="list-inline-item"><a href="https://github.com/minetest/contentdb">{{ _("Source Code") }}</a></li>

@ -36,7 +36,7 @@
{% endif %}
<a name="reply-{{ r.id }}" class="text-muted float-right"
href="{{ url_for('threads.view', id=thread.id) }}#reply-{{ r.id }}">
href="{{ r.get_url() }}">
{{ r.created_at | datetime }}
</a>
</div>
@ -48,10 +48,17 @@
<i class="fas fa-trash"></i>
</a>
{% endif %}
{% if current_user != r.author %}
<a class="float-right btn-secondary btn-sm ml-2"
title="{{ _('Report') }}"
href="{{ url_for('report.report', url=r.get_url()) }}">
<i class="fas fa-flag mr-1"></i>
</a>
{% endif %}
{% if current_user == thread.author and thread.review and thread.replies[0] == r %}
<a class="float-right btn btn-primary btn-sm ml-2"
href="{{ thread.review.package.getURL("packages.review") }}">
href="{{ thread.review.package.getURL('packages.review') }}">
<i class="fas fa-pen"></i>
</a>
{% elif r.checkPerm(current_user, "EDIT_REPLY") %}

@ -473,13 +473,16 @@
<p class="mt-3">
{% if package.approved and current_user != package.author %}
<a class="float-right"
href="{{ url_for('threads.new', pid=package.id) }}">
{{ _("Report a problem with this listing") }}
<a href="{{ url_for('report.report', url=url_current(True)) }}">
<i class="fas fa-flag mr-1"></i>
{{ _("Report") }}
</a>
{% endif %}
{% if package.checkPerm(current_user, "EDIT_PACKAGE") or package.checkPerm(current_user, "APPROVE_NEW") %}
<a class="float-right" href="{{ package.getURL("packages.audit") }}">
{% if package.approved and current_user != package.author %}
|
{% endif %}
<a href="{{ package.getURL("packages.audit") }}">
{{ _("See audit log") }}
</a>
{% endif %}

@ -0,0 +1,32 @@
{% extends "base.html" %}
{% block title %}
{{ _("Report") }}
{% endblock %}
{% block content %}
{% from "macros/forms.html" import render_field, render_submit_field, render_checkbox_field %}
<h1>{{ _("Report") }}</h1>
<form method="POST" action="" enctype="multipart/form-data">
{{ form.hidden_tag() }}
{% if url %}
<p>
URL: <code>{{ url }}</code>
</p>
{% else %}
{{ render_field(form.url, hint=_("URL to the thing you're reporting")) }}
{% endif %}
{{ render_field(form.message, hint=_("What are you reporting? Why are you reporting it?")) }}
{{ render_submit_field(form.submit) }}
<p class="mt-5 text-muted">
{{ _("Reports will be shared with ContentDB stuff.") }}
{% if is_anon %}
{{ _("Only the admin will be able to see who made the report.") }}
{% endif %}
</p>
</form>
{% endblock %}

@ -23,6 +23,11 @@
</a>
{% endif %}
<a class="btn btn-secondary float-right mr-3" href="{{ url_for('report.report', url=url_current(True)) }}">
<i class="fas fa-flag mr-1"></i>
{{ _("Report") }}
</a>
{% if current_user.is_authenticated and current_user.rank.atLeast(current_user.rank.MODERATOR) and user.email %}
<a class="btn btn-secondary float-right mr-3" href="{{ url_for('admin.send_single_email', username=user.username) }}">
<i class="fas fa-envelope mr-1"></i>

@ -40,6 +40,15 @@ def abs_url_for(path, **kwargs):
def abs_url(path):
return urljoin(app.config["BASE_URL"], path)
def url_current(abs=False):
args = MultiDict(request.args)
dargs = dict(args.lists())
dargs.update(request.view_args)
if abs:
return abs_url_for(request.endpoint, **dargs)
else:
return url_for(request.endpoint, **dargs)
def url_set_anchor(anchor):
args = MultiDict(request.args)
dargs = dict(args.lists())