mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-11-26 11:31:08 +00:00
commit
f1f7b21d43
19 changed files with 602 additions and 20 deletions
|
@ -495,7 +495,7 @@ class GroupForm(CustomForm):
|
||||||
class ReportForm(CustomForm):
|
class ReportForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Report
|
model = models.Report
|
||||||
fields = ["user", "reporter", "statuses", "links", "note"]
|
fields = ["user", "reporter", "status", "links", "note"]
|
||||||
|
|
||||||
|
|
||||||
class EmailBlocklistForm(CustomForm):
|
class EmailBlocklistForm(CustomForm):
|
||||||
|
@ -550,3 +550,9 @@ class ReadThroughForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.ReadThrough
|
model = models.ReadThrough
|
||||||
fields = ["user", "book", "start_date", "finish_date"]
|
fields = ["user", "book", "start_date", "finish_date"]
|
||||||
|
|
||||||
|
|
||||||
|
class AutoModRuleForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.AutoMod
|
||||||
|
fields = ["string_match", "flag_users", "flag_statuses", "created_by"]
|
||||||
|
|
39
bookwyrm/migrations/0138_automod.py
Normal file
39
bookwyrm/migrations/0138_automod.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# Generated by Django 3.2.12 on 2022-02-24 18:59
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0137_alter_sitesettings_allow_registration"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="AutoMod",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("string_match", models.CharField(max_length=200, unique=True)),
|
||||||
|
("flag_users", models.BooleanField(default=True)),
|
||||||
|
("flag_statuses", models.BooleanField(default=True)),
|
||||||
|
(
|
||||||
|
"created_by",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
45
bookwyrm/migrations/0139_report_status.py
Normal file
45
bookwyrm/migrations/0139_report_status.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
# Generated by Django 3.2.12 on 2022-02-24 20:41
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
def set_report_statuses(apps, schema_editor):
|
||||||
|
"""copy over status fields"""
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
report_model = apps.get_model("bookwyrm", "Report")
|
||||||
|
reports = report_model.objects.using(db_alias).filter(statuses__isnull=False)
|
||||||
|
for report in reports:
|
||||||
|
report.status = report.statuses.first()
|
||||||
|
report.save()
|
||||||
|
|
||||||
|
|
||||||
|
def set_reverse(apps, schema_editor):
|
||||||
|
"""copy over status fields"""
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
report_model = apps.get_model("bookwyrm", "Report")
|
||||||
|
reports = report_model.objects.using(db_alias).filter(status__isnull=False)
|
||||||
|
for report in reports:
|
||||||
|
report.statuses.set(report.status)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0138_automod"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="report",
|
||||||
|
name="status",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="reports",
|
||||||
|
to="bookwyrm.status",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(set_report_statuses, reverse_code=set_reverse),
|
||||||
|
]
|
17
bookwyrm/migrations/0140_remove_report_statuses.py
Normal file
17
bookwyrm/migrations/0140_remove_report_statuses.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 3.2.12 on 2022-02-24 20:43
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0139_report_status"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="report",
|
||||||
|
name="statuses",
|
||||||
|
),
|
||||||
|
]
|
24
bookwyrm/migrations/0141_alter_report_status.py
Normal file
24
bookwyrm/migrations/0141_alter_report_status.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# Generated by Django 3.2.12 on 2022-02-24 20:50
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0140_remove_report_statuses"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="report",
|
||||||
|
name="status",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="bookwyrm.status",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -29,7 +29,7 @@ from .import_job import ImportJob, ImportItem
|
||||||
from .site import SiteSettings, SiteInvite
|
from .site import SiteSettings, SiteInvite
|
||||||
from .site import PasswordReset, InviteRequest
|
from .site import PasswordReset, InviteRequest
|
||||||
from .announcement import Announcement
|
from .announcement import Announcement
|
||||||
from .antispam import EmailBlocklist, IPBlocklist
|
from .antispam import EmailBlocklist, IPBlocklist, AutoMod, automod_task
|
||||||
|
|
||||||
from .notification import Notification
|
from .notification import Notification
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
""" Lets try NOT to sell viagra """
|
""" Lets try NOT to sell viagra """
|
||||||
from django.db import models
|
from functools import reduce
|
||||||
|
import operator
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
|
from django.db import models
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from bookwyrm.tasks import app
|
||||||
from .user import User
|
from .user import User
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,3 +40,107 @@ class IPBlocklist(models.Model):
|
||||||
"""default sorting"""
|
"""default sorting"""
|
||||||
|
|
||||||
ordering = ("-created_date",)
|
ordering = ("-created_date",)
|
||||||
|
|
||||||
|
|
||||||
|
class AutoMod(models.Model):
|
||||||
|
"""rules to automatically flag suspicious activity"""
|
||||||
|
|
||||||
|
string_match = models.CharField(max_length=200, unique=True)
|
||||||
|
flag_users = models.BooleanField(default=True)
|
||||||
|
flag_statuses = models.BooleanField(default=True)
|
||||||
|
created_by = models.ForeignKey("User", on_delete=models.PROTECT)
|
||||||
|
|
||||||
|
|
||||||
|
@app.task(queue="low_priority")
|
||||||
|
def automod_task():
|
||||||
|
"""Create reports"""
|
||||||
|
if not AutoMod.objects.exists():
|
||||||
|
return
|
||||||
|
reporter = AutoMod.objects.first().created_by
|
||||||
|
reports = automod_users(reporter) + automod_statuses(reporter)
|
||||||
|
if reports:
|
||||||
|
admins = User.objects.filter(
|
||||||
|
models.Q(user_permissions__name__in=["moderate_user", "moderate_post"])
|
||||||
|
| models.Q(is_superuser=True)
|
||||||
|
).all()
|
||||||
|
notification_model = apps.get_model(
|
||||||
|
"bookwyrm", "Notification", require_ready=True
|
||||||
|
)
|
||||||
|
for admin in admins:
|
||||||
|
notification_model.objects.bulk_create(
|
||||||
|
[
|
||||||
|
notification_model(
|
||||||
|
user=admin,
|
||||||
|
related_report=r,
|
||||||
|
notification_type="REPORT",
|
||||||
|
)
|
||||||
|
for r in reports
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def automod_users(reporter):
|
||||||
|
"""check users for moderation flags"""
|
||||||
|
user_rules = AutoMod.objects.filter(flag_users=True).values_list(
|
||||||
|
"string_match", flat=True
|
||||||
|
)
|
||||||
|
if not user_rules:
|
||||||
|
return []
|
||||||
|
|
||||||
|
filters = []
|
||||||
|
for field in ["username", "summary", "name"]:
|
||||||
|
filters += [{f"{field}__icontains": r} for r in user_rules]
|
||||||
|
users = User.objects.filter(
|
||||||
|
reduce(operator.or_, (Q(**f) for f in filters)),
|
||||||
|
is_active=True,
|
||||||
|
local=True,
|
||||||
|
report__isnull=True, # don't flag users that already have reports
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
report_model = apps.get_model("bookwyrm", "Report", require_ready=True)
|
||||||
|
|
||||||
|
return report_model.objects.bulk_create(
|
||||||
|
[
|
||||||
|
report_model(
|
||||||
|
reporter=reporter,
|
||||||
|
note=_("Automatically generated report"),
|
||||||
|
user=u,
|
||||||
|
)
|
||||||
|
for u in users
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def automod_statuses(reporter):
|
||||||
|
"""check statues for moderation flags"""
|
||||||
|
status_rules = AutoMod.objects.filter(flag_statuses=True).values_list(
|
||||||
|
"string_match", flat=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not status_rules:
|
||||||
|
return []
|
||||||
|
|
||||||
|
filters = []
|
||||||
|
for field in ["content", "content_warning", "quotation__quote", "review__name"]:
|
||||||
|
filters += [{f"{field}__icontains": r} for r in status_rules]
|
||||||
|
|
||||||
|
status_model = apps.get_model("bookwyrm", "Status", require_ready=True)
|
||||||
|
statuses = status_model.objects.filter(
|
||||||
|
reduce(operator.or_, (Q(**f) for f in filters)),
|
||||||
|
deleted=False,
|
||||||
|
local=True,
|
||||||
|
report__isnull=True, # don't flag statuses that already have reports
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
report_model = apps.get_model("bookwyrm", "Report", require_ready=True)
|
||||||
|
return report_model.objects.bulk_create(
|
||||||
|
[
|
||||||
|
report_model(
|
||||||
|
reporter=reporter,
|
||||||
|
note=_("Automatically generated report"),
|
||||||
|
user=s.user,
|
||||||
|
status=s,
|
||||||
|
)
|
||||||
|
for s in statuses
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
|
@ -12,7 +12,12 @@ class Report(BookWyrmModel):
|
||||||
)
|
)
|
||||||
note = models.TextField(null=True, blank=True)
|
note = models.TextField(null=True, blank=True)
|
||||||
user = models.ForeignKey("User", on_delete=models.PROTECT)
|
user = models.ForeignKey("User", on_delete=models.PROTECT)
|
||||||
statuses = models.ManyToManyField("Status", blank=True)
|
status = models.ForeignKey(
|
||||||
|
"Status",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
)
|
||||||
links = models.ManyToManyField("Link", blank=True)
|
links = models.ManyToManyField("Link", blank=True)
|
||||||
resolved = models.BooleanField(default=False)
|
resolved = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
|
130
bookwyrm/templates/settings/automod/rules.html
Normal file
130
bookwyrm/templates/settings/automod/rules.html
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
{% extends 'settings/layout.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load utilities %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{% trans "Auto-moderation rules" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
{% trans "Auto-moderation rules" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block panel %}
|
||||||
|
|
||||||
|
<div class="notification content">
|
||||||
|
<p>
|
||||||
|
{% trans "Auto-moderation rules will create reports for any local user or status with fields matching the provided string." %}
|
||||||
|
{% trans "Users or statuses that have already been reported (regardless of whether the report was resolved) will not be flagged." %}
|
||||||
|
{% trans "At this time, reports are <em>not</em> being generated automatically, and you must manually trigger a scan." %}
|
||||||
|
</p>
|
||||||
|
<form name="run-scan" method="POST" action="{% url 'settings-automod-run' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button class="button is-warning">{% trans "Run scan" %}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if success %}
|
||||||
|
<div class="notification is-success is-light">
|
||||||
|
<span class="icon icon-check" aria-hidden="true"></span>
|
||||||
|
<span>
|
||||||
|
{% trans "Successfully added rule" %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="block content">
|
||||||
|
<h2 class="title is-4">{% trans "Add Rule" %}</h2>
|
||||||
|
<div class="box">
|
||||||
|
<form action="{% url 'settings-automod' %}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" value="{{ request.user.id }}" name="created_by">
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="id_string_match">{% trans "String match" %}</label>
|
||||||
|
{{ form.string_match }}
|
||||||
|
{% include 'snippets/form_errors.html' with errors_list=form.string_match.errors id="desc_string_match" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="id_flag_users">
|
||||||
|
{{ form.flag_users }}
|
||||||
|
{% trans "Flag users" %}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="id_flag_statuses">
|
||||||
|
{{ form.flag_statuses }}
|
||||||
|
{% trans "Flag statuses" %}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<button type="submit" class="button is-primary">{% trans "Add rule" %}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block content">
|
||||||
|
<h2 class="title is-4">{% trans "Current Rules" %}</h2>
|
||||||
|
<details class="details-panel">
|
||||||
|
<summary>
|
||||||
|
<span class="title is-5" role="heading" aria-level="3">
|
||||||
|
{% trans "Show rules" %} ({{ rules.count }})
|
||||||
|
</span>
|
||||||
|
<span class="details-close icon icon-x" aria-hidden="true"></span>
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="table is-striped">
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<label for="id_string_match">{% trans "String match" %}</label>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<label for="id_flag_users">{% trans "Flag users" %}</label>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<label for="id_flag_statuses">{% trans "Flag statuses" %}</label>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
{% for rule in rules %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>{{ rule.string_match }}</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ rule.flag_users|yesno }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ rule.flag_statuses|yesno }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<form action="{% url 'settings-automod-delete' rule.id %}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="control">
|
||||||
|
<button type="submit" class="button is-danger is-light is-small">
|
||||||
|
<span class="icon icon-x m-0-mobile" aria-hidden="true"></span>
|
||||||
|
<span class="is-sr-only-mobile">{% trans "Remove rule" %}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -56,6 +56,10 @@
|
||||||
{% url 'settings-reports' as url %}
|
{% url 'settings-reports' as url %}
|
||||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Reports" %}</a>
|
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Reports" %}</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
{% url 'settings-automod' as url %}
|
||||||
|
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Auto-moderation rules" %}</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
{% url 'settings-email-blocks' as url %}
|
{% url 'settings-email-blocks' as url %}
|
||||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Email Blocklist" %}</a>
|
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Email Blocklist" %}</a>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{% extends 'settings/layout.html' %}
|
{% extends 'settings/layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
|
{% load feed_page_tags %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% include "settings/reports/report_header.html" with report=report %}
|
{% include "settings/reports/report_header.html" with report=report %}
|
||||||
|
@ -30,20 +31,14 @@
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if report.statuses.exists %}
|
{% if report.status %}
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<h3 class="title is-4">{% trans "Reported statuses" %}</h3>
|
<h3 class="title is-4">{% trans "Reported status" %}</h3>
|
||||||
<ul>
|
{% if report.status.deleted %}
|
||||||
{% for status in report.statuses.select_subclasses.all %}
|
|
||||||
<li>
|
|
||||||
{% if status.deleted %}
|
|
||||||
<em>{% trans "Status has been deleted" %}</em>
|
<em>{% trans "Status has been deleted" %}</em>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% include 'snippets/status/status.html' with status=status moderation_mode=True %}
|
{% include 'snippets/status/status.html' with status=report.status|load_subclass moderation_mode=True %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load utilities %}
|
{% load utilities %}
|
||||||
|
|
||||||
{% if report.statuses.exists %}
|
{% if report.status %}
|
||||||
|
|
||||||
{% blocktrans trimmed with report_id=report.id username=report.user|username %}
|
{% blocktrans trimmed with report_id=report.id username=report.user|username %}
|
||||||
Report #{{ report_id }}: Status posted by @{{ username }}
|
Report #{{ report_id }}: Status posted by @{{ username }}
|
||||||
|
|
|
@ -12,6 +12,6 @@
|
||||||
>
|
>
|
||||||
{% trans "Report" %}
|
{% trans "Report" %}
|
||||||
</button>
|
</button>
|
||||||
{% include 'snippets/report_modal.html' with user=user id=modal_id status=status.id %}
|
{% include 'snippets/report_modal.html' with user=user id=modal_id status_id=status.id %}
|
||||||
|
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
<input type="hidden" name="reporter" value="{{ request.user.id }}">
|
<input type="hidden" name="reporter" value="{{ request.user.id }}">
|
||||||
<input type="hidden" name="user" value="{{ user.id }}">
|
<input type="hidden" name="user" value="{{ user.id }}">
|
||||||
{% if status_id %}
|
{% if status_id %}
|
||||||
<input type="hidden" name="statuses" value="{{ status_id }}">
|
<input type="hidden" name="status" value="{{ status_id }}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if link %}
|
{% if link %}
|
||||||
<input type="hidden" name="links" value="{{ link.id }}">
|
<input type="hidden" name="links" value="{{ link.id }}">
|
||||||
|
|
75
bookwyrm/tests/models/test_automod.py
Normal file
75
bookwyrm/tests/models/test_automod.py
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
""" test for app action functionality """
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
|
from bookwyrm import models
|
||||||
|
from bookwyrm.models.antispam import automod_task
|
||||||
|
|
||||||
|
|
||||||
|
@patch("bookwyrm.models.Status.broadcast")
|
||||||
|
@patch("bookwyrm.activitystreams.add_status_task.delay")
|
||||||
|
@patch("bookwyrm.activitystreams.remove_status_task.delay")
|
||||||
|
class AutomodModel(TestCase):
|
||||||
|
"""every response to a get request, html or json"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""we need basic test data and mocks"""
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
|
||||||
|
"bookwyrm.activitystreams.populate_stream_task.delay"
|
||||||
|
), patch("bookwyrm.lists_stream.populate_lists_task.delay"):
|
||||||
|
self.local_user = models.User.objects.create_user(
|
||||||
|
"mouse@local.com",
|
||||||
|
"mouse@mouse.mouse",
|
||||||
|
"password",
|
||||||
|
local=True,
|
||||||
|
localname="mouse",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_automod_task_no_rules(self, *_):
|
||||||
|
"""nothing to see here"""
|
||||||
|
self.assertFalse(models.Report.objects.exists())
|
||||||
|
automod_task()
|
||||||
|
self.assertFalse(models.Report.objects.exists())
|
||||||
|
|
||||||
|
def test_automod_task_user(self, *_):
|
||||||
|
"""scan activity"""
|
||||||
|
self.assertFalse(models.Report.objects.exists())
|
||||||
|
models.AutoMod.objects.create(
|
||||||
|
string_match="hi",
|
||||||
|
flag_users=True,
|
||||||
|
flag_statuses=True,
|
||||||
|
created_by=self.local_user,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.local_user.name = "okay hi"
|
||||||
|
self.local_user.save(broadcast=False, update_fields=["name"])
|
||||||
|
|
||||||
|
automod_task()
|
||||||
|
|
||||||
|
reports = models.Report.objects.all()
|
||||||
|
self.assertEqual(reports.count(), 1)
|
||||||
|
self.assertEqual(reports.first().user, self.local_user)
|
||||||
|
|
||||||
|
def test_automod_status(self, *_):
|
||||||
|
"""scan activity"""
|
||||||
|
self.assertFalse(models.Report.objects.exists())
|
||||||
|
models.AutoMod.objects.create(
|
||||||
|
string_match="hi",
|
||||||
|
flag_users=True,
|
||||||
|
flag_statuses=True,
|
||||||
|
created_by=self.local_user,
|
||||||
|
)
|
||||||
|
|
||||||
|
status = models.Status.objects.create(
|
||||||
|
user=self.local_user, content="hello", content_warning="hi"
|
||||||
|
)
|
||||||
|
|
||||||
|
automod_task()
|
||||||
|
|
||||||
|
reports = models.Report.objects.all()
|
||||||
|
self.assertEqual(reports.count(), 1)
|
||||||
|
self.assertEqual(reports.first().status, status)
|
||||||
|
self.assertEqual(reports.first().user, self.local_user)
|
58
bookwyrm/tests/views/admin/test_automod.py
Normal file
58
bookwyrm/tests/views/admin/test_automod.py
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
""" test for app action functionality """
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
|
from bookwyrm import forms, models, views
|
||||||
|
from bookwyrm.tests.validate_html import validate_html
|
||||||
|
|
||||||
|
|
||||||
|
class AutomodViews(TestCase):
|
||||||
|
"""every response to a get request, html or json"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""we need basic test data and mocks"""
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
|
||||||
|
"bookwyrm.activitystreams.populate_stream_task.delay"
|
||||||
|
), patch("bookwyrm.lists_stream.populate_lists_task.delay"):
|
||||||
|
self.local_user = models.User.objects.create_user(
|
||||||
|
"mouse@local.com",
|
||||||
|
"mouse@mouse.mouse",
|
||||||
|
"password",
|
||||||
|
local=True,
|
||||||
|
localname="mouse",
|
||||||
|
)
|
||||||
|
models.SiteSettings.objects.create()
|
||||||
|
|
||||||
|
def test_automod_rules_get(self):
|
||||||
|
"""there are so many views, this just makes sure it LOADS"""
|
||||||
|
view = views.AutoMod.as_view()
|
||||||
|
request = self.factory.get("")
|
||||||
|
request.user = self.local_user
|
||||||
|
request.user.is_superuser = True
|
||||||
|
|
||||||
|
result = view(request)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
validate_html(result.render())
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
def test_automod_rules_post(self):
|
||||||
|
"""there are so many views, this just makes sure it LOADS"""
|
||||||
|
form = forms.AutoModRuleForm()
|
||||||
|
form.data["string_match"] = "hello"
|
||||||
|
form.data["flag_users"] = True
|
||||||
|
form.data["flag_statuses"] = False
|
||||||
|
form.data["created_by"] = self.local_user
|
||||||
|
|
||||||
|
view = views.AutoMod.as_view()
|
||||||
|
request = self.factory.post("", form.data)
|
||||||
|
request.user = self.local_user
|
||||||
|
request.user.is_superuser = True
|
||||||
|
|
||||||
|
result = view(request)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
validate_html(result.render())
|
||||||
|
self.assertEqual(result.status_code, 200)
|
|
@ -214,6 +214,14 @@ urlpatterns = [
|
||||||
views.IPBlocklist.as_view(),
|
views.IPBlocklist.as_view(),
|
||||||
name="settings-ip-blocks-delete",
|
name="settings-ip-blocks-delete",
|
||||||
),
|
),
|
||||||
|
# auto-moderation rules
|
||||||
|
re_path(r"^settings/automod/?$", views.AutoMod.as_view(), name="settings-automod"),
|
||||||
|
re_path(
|
||||||
|
r"^settings/automod/(?P<rule_id>\d+)/delete?$",
|
||||||
|
views.automod_delete,
|
||||||
|
name="settings-automod-delete",
|
||||||
|
),
|
||||||
|
re_path(r"^settings/automod/run?$", views.run_automod, name="settings-automod-run"),
|
||||||
# moderation
|
# moderation
|
||||||
re_path(
|
re_path(
|
||||||
r"^settings/reports/?$", views.ReportsAdmin.as_view(), name="settings-reports"
|
r"^settings/reports/?$", views.ReportsAdmin.as_view(), name="settings-reports"
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
# site admin
|
# site admin
|
||||||
from .admin.announcements import Announcements, Announcement
|
from .admin.announcements import Announcements, Announcement
|
||||||
from .admin.announcements import EditAnnouncement, delete_announcement
|
from .admin.announcements import EditAnnouncement, delete_announcement
|
||||||
|
from .admin.automod import AutoMod, automod_delete, run_automod
|
||||||
from .admin.dashboard import Dashboard
|
from .admin.dashboard import Dashboard
|
||||||
from .admin.federation import Federation, FederatedServer
|
from .admin.federation import Federation, FederatedServer
|
||||||
from .admin.federation import AddFederatedServer, ImportServerBlocklist
|
from .admin.federation import AddFederatedServer, ImportServerBlocklist
|
||||||
|
|
64
bookwyrm/views/admin/automod.py
Normal file
64
bookwyrm/views/admin/automod.py
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
""" moderation via flagged posts and users """
|
||||||
|
from django.contrib.auth.decorators import login_required, permission_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 django.views.decorators.http import require_POST
|
||||||
|
|
||||||
|
from bookwyrm import forms, models
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(login_required, name="dispatch")
|
||||||
|
@method_decorator(
|
||||||
|
permission_required("bookwyrm.moderate_user", raise_exception=True),
|
||||||
|
name="dispatch",
|
||||||
|
)
|
||||||
|
@method_decorator(
|
||||||
|
permission_required("bookwyrm.moderate_post", raise_exception=True),
|
||||||
|
name="dispatch",
|
||||||
|
)
|
||||||
|
# pylint: disable=no-self-use
|
||||||
|
class AutoMod(View):
|
||||||
|
"""Manage automated flagging"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""view rules"""
|
||||||
|
data = {"rules": models.AutoMod.objects.all(), "form": forms.AutoModRuleForm()}
|
||||||
|
return TemplateResponse(request, "settings/automod/rules.html", data)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
"""add rule"""
|
||||||
|
form = forms.AutoModRuleForm(request.POST)
|
||||||
|
success = form.is_valid()
|
||||||
|
if success:
|
||||||
|
form.save()
|
||||||
|
form = forms.AutoModRuleForm()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"rules": models.AutoMod.objects.all(),
|
||||||
|
"form": form,
|
||||||
|
"success": success,
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, "settings/automod/rules.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
@require_POST
|
||||||
|
@permission_required("bookwyrm.moderate_user", raise_exception=True)
|
||||||
|
@permission_required("bookwyrm.moderate_post", raise_exception=True)
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def automod_delete(request, rule_id):
|
||||||
|
"""Remove a rule"""
|
||||||
|
rule = get_object_or_404(models.AutoMod, id=rule_id)
|
||||||
|
rule.delete()
|
||||||
|
return redirect("settings-automod")
|
||||||
|
|
||||||
|
|
||||||
|
@require_POST
|
||||||
|
@permission_required("bookwyrm.moderate_user", raise_exception=True)
|
||||||
|
@permission_required("bookwyrm.moderate_post", raise_exception=True)
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def run_automod(request):
|
||||||
|
"""run scan"""
|
||||||
|
models.automod_task.delay()
|
||||||
|
return redirect("settings-automod")
|
Loading…
Reference in a new issue