Allow users to report spam links

This commit is contained in:
Mouse Reeve 2022-01-10 14:55:10 -08:00
parent 651d468b13
commit 78dd5caf9f
20 changed files with 310 additions and 151 deletions

View file

@ -453,7 +453,7 @@ class GroupForm(CustomForm):
class ReportForm(CustomForm):
class Meta:
model = models.Report
fields = ["user", "reporter", "statuses", "note"]
fields = ["user", "reporter", "statuses", "links", "note"]
class EmailBlocklistForm(CustomForm):

View file

@ -0,0 +1,22 @@
# Generated by Django 3.2.10 on 2022-01-10 22:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0126_filelink_link_linkdomain"),
]
operations = [
migrations.RemoveConstraint(
model_name="report",
name="self_report",
),
migrations.AddField(
model_name="report",
name="links",
field=models.ManyToManyField(blank=True, to="bookwyrm.Link"),
),
]

View file

@ -16,10 +16,7 @@ class Link(ActivitypubMixin, BookWyrmModel):
url = fields.URLField(max_length=255, activitypub_field="href")
added_by = fields.ForeignKey(
"User",
on_delete=models.SET_NULL,
null=True,
activitypub_field="attributedTo"
"User", on_delete=models.SET_NULL, null=True, activitypub_field="attributedTo"
)
domain = models.ForeignKey(
"LinkDomain",

View file

@ -13,14 +13,12 @@ class Report(BookWyrmModel):
note = models.TextField(null=True, blank=True)
user = models.ForeignKey("User", on_delete=models.PROTECT)
statuses = models.ManyToManyField("Status", blank=True)
links = models.ManyToManyField("Link", blank=True)
resolved = models.BooleanField(default=False)
class Meta:
"""don't let users report themselves"""
"""set order by default"""
constraints = [
models.CheckConstraint(check=~Q(reporter=F("user")), name="self_report")
]
ordering = ("-created_date",)

View file

@ -720,6 +720,10 @@ ol.ordered-list li::before {
}
}
.overflow-wrap-anywhere {
overflow-wrap: anywhere;
}
/* Threads
******************************************************************************/

View file

@ -9,7 +9,8 @@
{% block modal-body %}
{% blocktrans trimmed with link_url=link.url %}
This link is taking you to <code>{{ link_url }}</code>. Is that where you'd like to go?
This link is taking you to: <code>{{ link_url }}</code>.<br>
Is that where you'd like to go?
{% endblocktrans %}
{% endblock %}
@ -19,4 +20,10 @@ This link is taking you to <code>{{ link_url }}</code>. Is that where you'd like
<a href="{{ link.url }}" target="_blank" rel="noopener" class="button is-primary">{% trans "Continue" %}</a>
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
{% if request.user.is_authenticated %}
<div class="has-text-right is-flex-grow-1">
<a href="{% url 'report-link' link.added_by.id link.id %}">{% trans "Report spam" %}</button>
</div>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,10 @@
{% extends "layout.html" %}
{% load i18n %}
{% block title %}
{% trans "Report" %}
{% endblock %}
{% block content %}
{% include "snippets/report_modal.html" with user=user active=True static=True %}
{% endblock %}

View file

@ -1,5 +1,4 @@
{% extends 'settings/layout.html' %}
{% load humanize %}
{% load i18n %}
{% load utilities %}
@ -32,7 +31,7 @@
{% for domain in domains %}
{% join "domain" domain.id as domain_modal %}
<div class="box content">
<div class="box content" id="{{ domain.id }}">
<div class="columns is-mobile">
<header class="column">
<h2 class="title is-5">
@ -58,37 +57,7 @@
</summary>
<div class="table-container mt-4">
<table class="is-striped">
<tr>
<th>{% trans "URL" %}</th>
<th>{% trans "Added by" %}</th>
<th>{% trans "Filetype" %}</th>
<th>{% trans "Book" %}</th>
</tr>
{% for link in domain.links.all|slice:10 %}
<tr>
<td>
<a href="{{ link.url }}" target="_blank" rel="noopener">{{ link.url }}</a>
</td>
<td>
<a href="{% url 'settings-user' link.added_by.id %}">@{{ link.added_by|username }}</a>
</td>
<td>
{% if link.filelink.filetype %}
{{ link.filelink.filetype }}
{% endif %}
</td>
<td>
{% if link.filelink.filetype %}
{% with book=link.filelink.book %}
<a href="{{ book.local_path }}">{% include "snippets/book_titleby.html" with book=book %}</a>
{% endwith %}
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% include "settings/link_domains/link_table.html" with links=domain.links.all|slice:10 %}
</div>
</details>
</div>

View file

@ -0,0 +1,36 @@
{% load i18n %}
{% load utilities %}
<table class="is-striped">
<tr>
<th>{% trans "URL" %}</th>
<th>{% trans "Added by" %}</th>
<th>{% trans "Filetype" %}</th>
<th>{% trans "Book" %}</th>
{% block additional_headers %}{% endblock %}
</tr>
{% for link in links%}
<tr>
<td class="overflow-wrap-anywhere">
<a href="{{ link.url }}" target="_blank" rel="noopener">{{ link.url }}</a>
</td>
<td>
<a href="{% url 'settings-user' link.added_by.id %}">@{{ link.added_by|username }}</a>
</td>
<td>
{% if link.filelink.filetype %}
{{ link.filelink.filetype }}
{% endif %}
</td>
<td>
{% if link.filelink.filetype %}
{% with book=link.filelink.book %}
<a href="{{ book.local_path }}">{% include "snippets/book_titleby.html" with book=book %}</a>
{% endwith %}
{% endif %}
</td>
{% block additional_data %}{% endblock %}
</tr>
{% endfor %}
</table>

View file

@ -2,10 +2,12 @@
{% load i18n %}
{% load humanize %}
{% block title %}{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}{% endblock %}
{% block title %}
{% include "settings/reports/report_header.html" with report=report %}
{% endblock %}
{% block header %}
{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}
{% include "settings/reports/report_header.html" with report=report %}
<a href="{% url 'settings-reports' %}" class="has-text-weight-normal help">{% trans "Back to reports" %}</a>
{% endblock %}
@ -15,6 +17,36 @@
{% include 'settings/reports/report_preview.html' with report=report %}
</div>
{% if report.statuses.exists %}
<div class="block">
<h3 class="title is-4">{% trans "Reported statuses" %}</h3>
<ul>
{% for status in report.statuses.select_subclasses.all %}
<li>
{% if status.deleted %}
<em>{% trans "Status has been deleted" %}</em>
{% else %}
{% include 'snippets/status/status.html' with status=status moderation_mode=True %}
{% endif %}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if report.links.exists %}
<div class="block">
<h3 class="title is-4">{% trans "Reported links" %}</h3>
<div class="card block">
<div class="card-content content">
<div class="table-container">
{% include "settings/reports/report_links_table.html" with links=report.links.all %}
</div>
</div>
</div>
</div>
{% endif %}
{% include 'settings/users/user_info.html' with user=report.user %}
{% include 'settings/users/user_moderation_actions.html' with user=report.user %}
@ -41,23 +73,4 @@
<button class="button">{% trans "Comment" %}</button>
</form>
</div>
<div class="block">
<h3 class="title is-4">{% trans "Reported statuses" %}</h3>
{% if not report.statuses.exists %}
<em>{% trans "No statuses reported" %}</em>
{% else %}
<ul>
{% for status in report.statuses.select_subclasses.all %}
<li>
{% if status.deleted %}
<em>{% trans "Status has been deleted" %}</em>
{% else %}
{% include 'snippets/status/status.html' with status=status moderation_mode=True %}
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endblock %}

View file

@ -0,0 +1,22 @@
{% load i18n %}
{% load utilities %}
{% if report.statuses.exists %}
{% blocktrans trimmed with report_id=report.id username=report.user|username %}
Report #{{ report_id }}: Status posted by @{{ username }}
{% endblocktrans %}
{% elif report.links.exists %}
{% blocktrans trimmed with report_id=report.id username=report.user|username %}
Report #{{ report_id }}: Link added by @{{ username }}
{% endblocktrans %}
{% else %}
{% blocktrans trimmed with report_id=report.id username=report.user|username %}
Report #{{ report_id }}: User @{{ username }}
{% endblocktrans %}
{% endif %}

View file

@ -0,0 +1,20 @@
{% extends "settings/link_domains/link_table.html" %}
{% load i18n %}
{% block additional_headers %}
<th>{% trans "Domain" %}</th>
<th>{% trans "Actions" %}</th>
{% endblock %}
{% block additional_data %}
<td>
<a href="{% url 'settings-link-domain' 'pending' %}#{{ link.domain.id }}">
{{ link.domain.domain }}
</a>
</td>
<td>
<form>
<button type="submit" class="button is-danger is-light">{% trans "Block domain" %}</button>
</form>
</td>
{% endblock %}

View file

@ -1,9 +1,13 @@
{% extends 'components/card.html' %}
{% load i18n %}
{% load humanize %}
{% load utilities %}
{% block card-header %}
<h2 class="card-header-title has-background-white-ter is-block">
<a href="{% url 'settings-report' report.id %}">{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}</a>
<a href="{% url 'settings-report' report.id %}">
{% include "settings/reports/report_header.html" with report=report %}
</a>
</h2>
{% endblock %}
@ -17,7 +21,7 @@
{% block card-footer %}
<div class="card-footer-item">
<p>{% blocktrans with username=report.reporter.display_name path=report.reporter.local_path %}Reported by <a href="{{ path }}">{{ username }}</a>{% endblocktrans %}</p>
<p>{% blocktrans with username=report.reporter|username path=report.reporter.local_path %}Reported by <a href="{{ path }}">@{{ username }}</a>{% endblocktrans %}</p>
</div>
<div class="card-footer-item">
{{ report.created_date | naturaltime }}

View file

@ -5,69 +5,71 @@
{% trans "Permanently deleted" %}
</div>
{% else %}
<h3>{% trans "Actions" %}</h3>
<h3>{% trans "User Actions" %}</h3>
<div class="is-flex">
{% if user.is_active %}
<p class="mr-1">
<a class="button" href="{% url 'direct-messages-user' user.username %}">{% trans "Send direct message" %}</a>
</p>
{% endif %}
<div class="box">
<div class="is-flex">
{% if user.is_active %}
<p class="mr-1">
<a class="button" href="{% url 'direct-messages-user' user.username %}">{% trans "Send direct message" %}</a>
</p>
{% endif %}
{% if user.is_active or user.deactivation_reason == "pending" %}
<form name="suspend" method="post" action="{% url 'settings-report-suspend' user.id %}" class="mr-1">
{% csrf_token %}
<button type="submit" class="button is-danger is-light">{% trans "Suspend user" %}</button>
</form>
{% else %}
<form name="unsuspend" method="post" action="{% url 'settings-report-unsuspend' user.id %}" class="mr-1">
{% csrf_token %}
<button class="button">{% trans "Un-suspend user" %}</button>
</form>
{% if user.is_active or user.deactivation_reason == "pending" %}
<form name="suspend" method="post" action="{% url 'settings-report-suspend' user.id %}" class="mr-1">
{% csrf_token %}
<button type="submit" class="button is-danger is-light">{% trans "Suspend user" %}</button>
</form>
{% else %}
<form name="unsuspend" method="post" action="{% url 'settings-report-unsuspend' user.id %}" class="mr-1">
{% csrf_token %}
<button class="button">{% trans "Un-suspend user" %}</button>
</form>
{% endif %}
{% if user.local %}
<div>
{% trans "Permanently delete user" as button_text %}
{% include "snippets/toggle/open_button.html" with controls_text="delete_user" text=button_text class="is-danger is-light" %}
</div>
{% endif %}
</div>
{% if user.local %}
<div>
{% include "settings/users/delete_user_form.html" with controls_text="delete_user" class="mt-2 mb-2" %}
</div>
{% endif %}
{% if user.local %}
<div>
{% trans "Permanently delete user" as button_text %}
{% include "snippets/toggle/open_button.html" with controls_text="delete_user" text=button_text class="is-danger is-light" %}
<form name="permission" method="post" action="{% url 'settings-user' user.id %}">
{% csrf_token %}
<label class="label" for="id_user_group">{% trans "Access level:" %}</label>
{% if group_form.non_field_errors %}
{{ group_form.non_field_errors }}
{% endif %}
{% with group=user.groups.first %}
<div class="select">
<select name="groups" id="id_user_group" aria-describedby="desc_user_group">
{% for value, name in group_form.fields.groups.choices %}
<option value="{{ value }}" {% if name == group.name %}selected{% endif %}>
{{ name|title }}
</option>
{% endfor %}
<option value="" {% if not group %}selected{% endif %}>
User
</option>
</select>
</div>
{% include 'snippets/form_errors.html' with errors_list=group_form.groups.errors id="desc_user_group" %}
{% endwith %}
<button class="button">
{% trans "Save" %}
</button>
</form>
</div>
{% endif %}
</div>
{% if user.local %}
<div>
{% include "settings/users/delete_user_form.html" with controls_text="delete_user" class="mt-2 mb-2" %}
</div>
{% endif %}
{% if user.local %}
<div>
<form name="permission" method="post" action="{% url 'settings-user' user.id %}">
{% csrf_token %}
<label class="label" for="id_user_group">{% trans "Access level:" %}</label>
{% if group_form.non_field_errors %}
{{ group_form.non_field_errors }}
{% endif %}
{% with group=user.groups.first %}
<div class="select">
<select name="groups" id="id_user_group" aria-describedby="desc_user_group">
{% for value, name in group_form.fields.groups.choices %}
<option value="{{ value }}" {% if name == group.name %}selected{% endif %}>
{{ name|title }}
</option>
{% endfor %}
<option value="" {% if not group %}selected{% endif %}>
User
</option>
</select>
</div>
{% include 'snippets/form_errors.html' with errors_list=group_form.groups.errors id="desc_user_group" %}
{% endwith %}
<button class="button">
{% trans "Save" %}
</button>
</form>
</div>
{% endif %}

View file

@ -12,6 +12,6 @@
>
{% trans "Report" %}
</button>
{% include 'snippets/report_modal.html' with user=user reporter=request.user id=modal_id %}
{% include 'snippets/report_modal.html' with user=user id=modal_id status=status.id %}
{% endwith %}

View file

@ -1,9 +1,16 @@
{% extends 'components/modal.html' %}
{% load i18n %}
{% load utilities %}
{% load humanize %}
{% block modal-title %}
{% blocktrans with username=user.username %}Report @{{ username }}{% endblocktrans %}
{% if status %}
{% blocktrans with username=user|username %}Report @{{ username }}'s status{% endblocktrans %}
{% elif link %}
{% blocktrans with domain=link.domain.domain %}Report {{ domain }} link{% endblocktrans %}
{% else %}
{% blocktrans with username=user|username %}Report @{{ username }}{% endblocktrans %}
{% endif %}
{% endblock %}
{% block modal-form-open %}
@ -13,14 +20,22 @@
{% block modal-body %}
{% csrf_token %}
<input type="hidden" name="reporter" value="{{ reporter.id }}">
<input type="hidden" name="reporter" value="{{ request.user.id }}">
<input type="hidden" name="user" value="{{ user.id }}">
{% if status %}
<input type="hidden" name="statuses" value="{{ status.id }}">
<input type="hidden" name="statuses" value="{{ status_id }}">
{% endif %}
{% if link %}
<input type="hidden" name="links" value="{{ link.id }}">
{% endif %}
<section class="content">
<p>{% blocktrans with site_name=site.name %}This report will be sent to {{ site_name }}'s moderators for review.{% endblocktrans %}</p>
<p class="notification">
{% blocktrans with site_name=site.name %}This report will be sent to {{ site_name }}'s moderators for review.{% endblocktrans %}
{% if link %}
{% trans "Links from this domain will be removed until your report has been reviewed." %}
{% endif %}
</p>
<div class="control">
<label class="label" for="id_{{ controls_uid }}_report_note">
{% trans "More info about this report:" %}
@ -35,7 +50,9 @@
{% block modal-footer %}
<button class="button is-success" type="submit">{% trans "Submit" %}</button>
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
{% if not static %}
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
{% endif %}
{% endblock %}

View file

@ -184,10 +184,12 @@ urlpatterns = [
name="settings-ip-blocks-delete",
),
# moderation
re_path(r"^settings/reports/?$", views.Reports.as_view(), name="settings-reports"),
re_path(
r"^settings/reports/?$", views.ReportsAdmin.as_view(), name="settings-reports"
),
re_path(
r"^settings/reports/(?P<report_id>\d+)/?$",
views.Report.as_view(),
views.ReportAdmin.as_view(),
name="settings-report",
),
re_path(
@ -210,7 +212,18 @@ urlpatterns = [
views.resolve_report,
name="settings-report-resolve",
),
re_path(r"^report/?$", views.make_report, name="report"),
re_path(r"^report/?$", views.Report.as_view(), name="report"),
re_path(r"^report/(?P<user_id>\d+)/?$", views.Report.as_view(), name="report"),
re_path(
r"^report/(?P<user_id>\d+)/status/(?P<status_id>\d+)?$",
views.Report.as_view(),
name="report-status",
),
re_path(
r"^report/(?P<user_id>\d+)/link/(?P<link_id>\d+)?$",
views.Report.as_view(),
name="report-link",
),
# landing pages
re_path(r"^about/?$", views.about, name="about"),
re_path(r"^privacy/?$", views.privacy, name="privacy"),

View file

@ -11,9 +11,8 @@ from .admin.invite import ManageInvites, Invite, InviteRequest
from .admin.invite import ManageInviteRequests, ignore_invite_request
from .admin.link_domains import LinkDomain, update_domain_status
from .admin.reports import (
Report,
Reports,
make_report,
ReportAdmin,
ReportsAdmin,
resolve_report,
suspend_user,
unsuspend_user,
@ -97,6 +96,7 @@ from .notifications import Notifications
from .outbox import Outbox
from .reading import create_readthrough, delete_readthrough, delete_progressupdate
from .reading import ReadingStatus
from .report import Report
from .rss_feed import RssFeed
from .search import Search
from .status import CreateStatus, EditStatus, DeleteStatus, update_progress

View file

@ -5,9 +5,8 @@ from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.http import require_POST
from bookwyrm import emailing, forms, models
from bookwyrm import forms, models
# pylint: disable=no-self-use
@ -20,7 +19,7 @@ from bookwyrm import emailing, forms, models
permission_required("bookwyrm.moderate_post", raise_exception=True),
name="dispatch",
)
class Reports(View):
class ReportsAdmin(View):
"""list of reports"""
def get(self, request):
@ -52,7 +51,7 @@ class Reports(View):
permission_required("bookwyrm.moderate_post", raise_exception=True),
name="dispatch",
)
class Report(View):
class ReportAdmin(View):
"""view a specific report"""
def get(self, request, report_id):
@ -132,16 +131,3 @@ def resolve_report(_, report_id):
if not report.resolved:
return redirect("settings-report", report.id)
return redirect("settings-reports")
@login_required
@require_POST
def make_report(request):
"""a user reports something"""
form = forms.ReportForm(request.POST)
if not form.is_valid():
raise ValueError(form.errors)
report = form.save()
emailing.moderation_report_email(report)
return redirect(request.headers.get("Referer", "/"))

39
bookwyrm/views/report.py Normal file
View file

@ -0,0 +1,39 @@
""" moderation via flagged posts and users """
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views import View
from bookwyrm import emailing, forms, models
# pylint: disable=no-self-use
@method_decorator(login_required, name="dispatch")
class Report(View):
"""Make reports"""
def get(self, request, user_id, status_id=None, link_id=None):
"""static view of report modal"""
data = {"user": get_object_or_404(models.User, id=user_id)}
if status_id:
data["status"] = status_id
if link_id:
data["link"] = get_object_or_404(models.Link, id=link_id)
return TemplateResponse(request, "report.html", data)
def post(self, request):
"""a user reports something"""
form = forms.ReportForm(request.POST)
if not form.is_valid():
raise ValueError(form.errors)
report = form.save()
if report.links.exists():
# revert the domain to pending
domain = report.links.first().domain
domain.status = "pending"
domain.save()
emailing.moderation_report_email(report)
return redirect(request.headers.get("Referer", "/"))