This commit is contained in:
Mouse Reeve 2024-04-04 15:25:16 +02:00 committed by GitHub
commit 641a805986
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 301 additions and 49 deletions

View file

@ -24,6 +24,7 @@ from .verbs import Follow, Accept, Reject, Block
from .verbs import Add, Remove
from .verbs import Announce, Like
from .verbs import Move
from .verbs import Flag
# this creates a list of all the Activity types that we can serialize,
# so when an Activity comes in from outside, we can check if it's known

View file

@ -268,3 +268,13 @@ class Move(Verb):
else:
# we might do something with this to move other objects at some point
pass
@dataclass(init=False)
class Flag(Verb):
"""Report a user to their home server"""
to: str
object: List[str] = None
links: List[str] = None
type: str = "Flag"

View file

@ -50,9 +50,11 @@ def password_reset_email(reset_code):
def moderation_report_email(report):
"""a report was created"""
data = email_data()
data["reporter"] = report.reporter.localname or report.reporter.username
if report.user:
data["reportee"] = report.user.localname or report.user.username
data["reporter"] = report.user.localname or report.user.username
if report.reported_user:
data["reportee"] = (
report.reported_user.localname or report.reported_user.username
)
data["report_link"] = report.remote_id
data["link_domain"] = report.links.exists()

View file

@ -44,7 +44,7 @@ class GoalForm(CustomForm):
class ReportForm(CustomForm):
class Meta:
model = models.Report
fields = ["user", "reporter", "status", "links", "note"]
fields = ["reported_user", "user", "status", "links", "note", "allow_broadcast"]
class ReadThroughForm(CustomForm):

View file

@ -0,0 +1,57 @@
# Generated by Django 3.2.23 on 2024-01-02 21:56
import bookwyrm.models.fields
from django.conf import settings
from django.db import migrations
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0191_merge_20240102_0326"),
]
operations = [
migrations.AlterField(
model_name="report",
name="links",
field=bookwyrm.models.fields.ManyToManyField(
blank=True, to="bookwyrm.Link"
),
),
migrations.AlterField(
model_name="report",
name="note",
field=bookwyrm.models.fields.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name="report",
name="reporter",
field=bookwyrm.models.fields.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="reporter",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name="report",
name="status",
field=bookwyrm.models.fields.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="bookwyrm.status",
),
),
migrations.AlterField(
model_name="report",
name="user",
field=bookwyrm.models.fields.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to=settings.AUTH_USER_MODEL,
),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.23 on 2024-01-02 22:16
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0192_auto_20240102_2156"),
]
operations = [
migrations.RenameField(
model_name="report",
old_name="user",
new_name="reported_user",
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.23 on 2024-01-02 22:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0193_rename_user_report_reported_user"),
]
operations = [
migrations.RenameField(
model_name="report",
old_name="reporter",
new_name="user",
),
]

View file

@ -0,0 +1,39 @@
# Generated by Django 3.2.23 on 2024-01-02 23:10
import bookwyrm.models.fields
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0194_rename_reporter_report_user"),
]
operations = [
migrations.AddField(
model_name="report",
name="allow_broadcast",
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name="report",
name="reported_user",
field=bookwyrm.models.fields.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="reported_user",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name="report",
name="user",
field=bookwyrm.models.fields.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL
),
),
]

View file

@ -109,9 +109,9 @@ def automod_users(reporter):
return report_model.objects.bulk_create(
[
report_model(
reporter=reporter,
user=reporter,
note=_("Automatically generated report"),
user=u,
reported_user=u,
)
for u in users
]
@ -143,9 +143,9 @@ def automod_statuses(reporter):
return report_model.objects.bulk_create(
[
report_model(
reporter=reporter,
user=reporter,
note=_("Automatically generated report"),
user=s.user,
reported_user=s.user,
status=s,
)
for s in statuses

View file

@ -1,10 +1,12 @@
""" flagged for moderation """
from django.core.exceptions import PermissionDenied
from django.db import models
from django.utils.translation import gettext_lazy as _
from bookwyrm import activitypub
from bookwyrm.settings import DOMAIN
from .activitypub_mixin import ActivityMixin
from .base_model import BookWyrmModel
from . import fields
# Report action enums
@ -22,28 +24,49 @@ APPROVE_DOMAIN = "approve_domain"
DELETE_ITEM = "delete_item"
class Report(BookWyrmModel):
class Report(ActivityMixin, BookWyrmModel):
"""reported status or user"""
reporter = models.ForeignKey(
"User", related_name="reporter", on_delete=models.PROTECT
activity_serializer = activitypub.Flag
user = fields.ForeignKey(
"User",
on_delete=models.PROTECT,
activitypub_field="actor",
)
note = models.TextField(null=True, blank=True)
user = models.ForeignKey("User", on_delete=models.PROTECT, null=True, blank=True)
status = models.ForeignKey(
note = fields.TextField(null=True, blank=True, activitypub_field="content")
reported_user = fields.ForeignKey(
"User",
related_name="reported_user",
on_delete=models.PROTECT,
null=True,
blank=True,
activitypub_field="to",
)
status = fields.ForeignKey(
"Status",
null=True,
blank=True,
on_delete=models.PROTECT,
activitypub_field="object",
)
links = models.ManyToManyField("Link", blank=True)
links = fields.ManyToManyField("Link", blank=True)
resolved = models.BooleanField(default=False)
allow_broadcast = models.BooleanField(default=False)
def raise_not_editable(self, viewer):
"""instead of user being the owner field, it's reporter"""
if self.reporter == viewer or viewer.has_perm("bookwyrm.moderate_user"):
def broadcast(self, activity, sender, *args, **kwargs):
"""only need to send an activity for remote offenders"""
# don't try to broadcast if the reporter doesn't want you to,
# or if the reported user is local
if self.reported_user.local or not self.allow_broadcast:
return
raise PermissionDenied()
super().broadcast(activity, sender, *args, **kwargs)
def get_recipients(self, software=None):
"""Send this to the public inbox of the offending instance"""
if self.reported_user.local:
return []
return [self.reported_user.shared_inbox or self.reported_user.inbox]
def get_remote_id(self):
return f"https://{DOMAIN}/settings/reports/{self.id}"

View file

@ -6,5 +6,5 @@
{% endblock %}
{% block content %}
{% include "snippets/report_modal.html" with user=user active=True static=True id="report-modal" %}
{% include "snippets/report_modal.html" with reported_user=reported_user active=True static=True id="report-modal" %}
{% endblock %}

View file

@ -27,7 +27,7 @@
</summary>
<div class="box">
{% trans "Update on your report:" as dm_template %}
{% include 'snippets/create_status/status.html' with type="direct" uuid=1 mention=report.reporter prepared_content=dm_template no_script=True %}
{% include 'snippets/create_status/status.html' with type="direct" uuid=1 mention=report.user prepared_content=dm_template no_script=True %}
</div>
</details>
</div>
@ -56,10 +56,10 @@
</div>
{% endif %}
{% if report.user %}
{% include 'settings/users/user_info.html' with user=report.user %}
{% if report.reported_user %}
{% include 'settings/users/user_info.html' with user=report.reported_user %}
{% include 'settings/users/user_moderation_actions.html' with user=report.user %}
{% include 'settings/users/user_moderation_actions.html' with user=report.reported_user %}
{% endif %}
<div class="block content">
@ -70,8 +70,8 @@
<li class="mb-2">
<div class="is-flex">
<p class="mb-0 is-flex-grow-1">
{% blocktrans trimmed with user=report.reporter|username user_link=report.reporter.local_path %}
<a href="{{ user_link }}">{{ user}}</a> opened this report
{% blocktrans trimmed with user=report.user|username user_link=report.user.local_path %}
<a href="{{ user_link }}">{{ user }}</a> opened this report
{% endblocktrans %}
</p>
<span class="tag">{{ report.created_date }}</span>

View file

@ -3,14 +3,14 @@
{% if report.status %}
{% blocktrans trimmed with report_id=report.id username=report.user|username %}
{% blocktrans trimmed with report_id=report.id username=report.reported_user|username %}
Report #{{ report_id }}: Status posted by @{{ username }}
{% endblocktrans %}
{% elif report.links.exists %}
{% if report.user %}
{% blocktrans trimmed with report_id=report.id username=report.user|username %}
{% if report.reported_user %}
{% blocktrans trimmed with report_id=report.id username=report.reported_user|username %}
Report #{{ report_id }}: Link added by @{{ username }}
{% endblocktrans %}
{% else %}
@ -21,7 +21,7 @@ Report #{{ report_id }}: Status posted by @{{ username }}
{% else %}
{% blocktrans trimmed with report_id=report.id username=report.user|username %}
{% blocktrans trimmed with report_id=report.id username=report.reported_user|username %}
Report #{{ report_id }}: User @{{ username }}
{% endblocktrans %}

View file

@ -21,8 +21,17 @@
{% block card-footer %}
<div class="card-footer-item">
<p>{% blocktrans with username=report.reporter|username path=report.reporter.local_path %}Reported by <a href="{{ path }}">@{{ username }}</a>{% endblocktrans %}</p>
<p>{% blocktrans with username=report.user|username path=report.user.local_path %}Reported by <a href="{{ path }}">@{{ username }}</a>{% endblocktrans %}</p>
</div>
{% if not report.reported_user.local %}
<div class="card-footer-item">
{% if report.allow_broadcast %}
<p>{% trans "Sent to remote instance" %}</p>
{% else %}
<p>{% trans "<b>Not</b> sent to remote instance" %}</p>
{% endif %}
</div>
{% endif %}
<div class="card-footer-item">
{{ report.created_date | naturaltime }}
</div>

View file

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

View file

@ -5,11 +5,11 @@
{% block modal-title %}
{% if status %}
{% blocktrans with username=user|username %}Report @{{ username }}'s status{% endblocktrans %}
{% blocktrans with username=reported_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 %}
{% blocktrans with username=reported_user|username %}Report @{{ username }}{% endblocktrans %}
{% endif %}
{% endblock %}
@ -20,8 +20,8 @@
{% block modal-body %}
{% csrf_token %}
<input type="hidden" name="reporter" value="{{ request.user.id }}">
<input type="hidden" name="user" value="{{ user.id }}">
<input type="hidden" name="user" value="{{ request.user.id }}">
<input type="hidden" name="reported_user" value="{{ reported_user.id }}">
{% if status_id %}
<input type="hidden" name="status" value="{{ status_id }}">
{% endif %}
@ -36,6 +36,14 @@
{% trans "Links from this domain will be removed until your report has been reviewed." %}
{% endif %}
</p>
{% if not reported_user.local %}
<label class="checkbox label">
<input class="checkbox" id="id_allow_broadcast" type="checkbox" name="allow_broadcast" checked>
{% trans "Also send this report to the user's instance admins" %}
</label>
{% else %}
<input type="hidden" name="allow_broadcast">
{% endif %}
<div class="control">
<label class="label" for="id_{{ controls_uid }}_report_note">
{% trans "More info about this report:" %}

View file

@ -0,0 +1,61 @@
""" testing models """
from unittest.mock import patch
from django.test import TestCase
from bookwyrm import models
class Relationship(TestCase):
"""following, blocking, stuff like that"""
@classmethod
def setUpTestData(self): # pylint: disable=bad-classmethod-argument, invalid-name
"""we need some users for this"""
with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user(
"rat",
"rat@rat.com",
"ratword",
local=False,
remote_id="https://example.com/users/rat",
inbox="https://example.com/users/rat/inbox",
outbox="https://example.com/users/rat/outbox",
)
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", "mouse@mouse.com", "mouseword", local=True, localname="mouse"
)
self.another_local_user = models.User.objects.create_user(
"bird", "bird@bird.com", "birdword", local=True, localname="bird"
)
self.local_user.remote_id = "http://local.com/user/mouse"
self.local_user.save(broadcast=False, update_fields=["remote_id"])
def test_report_local_user(self):
"""a report/flag within an instance"""
report = models.Report.objects.create(
user=self.local_user,
note="oh no bad",
reported_user=self.another_local_user,
)
activity = report.to_activity()
self.assertEqual(activity["type"], "Flag")
self.assertEqual(activity["actor"], self.local_user.remote_id)
self.assertEqual(activity["to"], self.another_local_user.remote_id)
# self.assertEqual(activity["object"], [self.another_local_user.remote_id])
self.assertEqual(report.get_recipients(), [])
def test_report_remote_user_with_broadcast(self):
"""a report to the outside needs to broadcast"""
with patch("bookwyrm.models.report.Report.broadcast") as broadcast_mock:
report = models.Report.objects.create(
user=self.local_user,
note="oh no bad",
reported_user=self.remote_user,
allow_broadcast=True,
)
self.assertEqual(broadcast_mock.call_count, 1)
self.assertEqual(report.get_recipients(), [self.remote_user.inbox])

View file

@ -63,7 +63,7 @@ class ReportViews(TestCase):
view = views.ReportsAdmin.as_view()
request = self.factory.get("")
request.user = self.local_user
models.Report.objects.create(reporter=self.local_user, user=self.rat)
models.Report.objects.create(user=self.local_user, reported_user=self.rat)
result = view(request)
self.assertIsInstance(result, TemplateResponse)
@ -75,7 +75,9 @@ class ReportViews(TestCase):
view = views.ReportAdmin.as_view()
request = self.factory.get("")
request.user = self.local_user
report = models.Report.objects.create(reporter=self.local_user, user=self.rat)
report = models.Report.objects.create(
user=self.local_user, reported_user=self.rat
)
result = view(request, report.id)
@ -88,7 +90,9 @@ class ReportViews(TestCase):
view = views.ReportAdmin.as_view()
request = self.factory.post("", {"note": "hi"})
request.user = self.local_user
report = models.Report.objects.create(reporter=self.local_user, user=self.rat)
report = models.Report.objects.create(
user=self.local_user, reported_user=self.rat
)
view(request, report.id)
@ -100,7 +104,9 @@ class ReportViews(TestCase):
def test_resolve_report(self):
"""toggle report resolution status"""
report = models.Report.objects.create(reporter=self.local_user, user=self.rat)
report = models.Report.objects.create(
user=self.local_user, reported_user=self.rat
)
self.assertFalse(report.resolved)
self.assertFalse(models.ReportAction.objects.exists())
request = self.factory.post("")

View file

@ -98,7 +98,7 @@ class UserAdminViews(TestCase):
)
report = models.Report.objects.create(
user=self.local_user, reporter=self.local_user
reported_user=self.local_user, user=self.local_user
)
view = views.UserAdmin.as_view()

View file

@ -115,8 +115,8 @@ class NotificationViews(TestCase):
def test_notifications_page_report(self):
"""import completed notification"""
report = models.Report.objects.create(
user=self.another_user,
reporter=self.local_user,
reported_user=self.another_user,
user=self.local_user,
)
notification = models.Notification.objects.create(
user=self.local_user,

View file

@ -80,15 +80,15 @@ class ReportViews(TestCase):
def test_make_report(self):
"""a user reports another user"""
form = forms.ReportForm()
form.data["reporter"] = self.local_user.id
form.data["user"] = self.rat.id
form.data["user"] = self.local_user.id
form.data["reported_user"] = self.rat.id
request = self.factory.post("", form.data)
request.user = self.local_user
views.Report.as_view()(request)
report = models.Report.objects.get()
self.assertEqual(report.reporter, self.local_user)
self.assertEqual(report.user, self.local_user)
self.assertEqual(report.user, self.rat)
def test_report_link(self):
@ -102,8 +102,8 @@ class ReportViews(TestCase):
domain.save()
form = forms.ReportForm()
form.data["reporter"] = self.local_user.id
form.data["user"] = self.rat.id
form.data["user"] = self.local_user.id
form.data["reported_user"] = self.rat.id
form.data["links"] = link.id
request = self.factory.post("", form.data)
request.user = self.local_user