mirror of
https://github.com/jointakahe/takahe.git
synced 2024-12-03 04:16:27 +00:00
Report function and admin
This commit is contained in:
parent
b3b2c6effd
commit
e8d6dccbb2
16 changed files with 615 additions and 4 deletions
|
@ -251,6 +251,7 @@ class Post(StatorModel):
|
||||||
action_unboost = "{view}unboost/"
|
action_unboost = "{view}unboost/"
|
||||||
action_delete = "{view}delete/"
|
action_delete = "{view}delete/"
|
||||||
action_edit = "{view}edit/"
|
action_edit = "{view}edit/"
|
||||||
|
action_report = "{view}report/"
|
||||||
action_reply = "/compose/?reply_to={self.id}"
|
action_reply = "/compose/?reply_to={self.id}"
|
||||||
admin_edit = "/djadmin/activities/post/{self.id}/change/"
|
admin_edit = "/djadmin/activities/post/{self.id}/change/"
|
||||||
|
|
||||||
|
|
|
@ -709,7 +709,9 @@ button,
|
||||||
}
|
}
|
||||||
|
|
||||||
button.delete,
|
button.delete,
|
||||||
.button.delete {
|
.button.delete,
|
||||||
|
button.danger,
|
||||||
|
.button.danger {
|
||||||
background: var(--color-delete);
|
background: var(--color-delete);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -833,6 +835,20 @@ table.metadata th {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table.buttons {
|
||||||
|
margin: -10px 0 10px 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.buttons th {
|
||||||
|
padding: 5px 20px 5px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.buttons th button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.stats {
|
.stats {
|
||||||
margin: 0 0 20px 0;
|
margin: 0 0 20px 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ from api.views import api_router, oauth
|
||||||
from core import views as core
|
from core import views as core
|
||||||
from mediaproxy import views as mediaproxy
|
from mediaproxy import views as mediaproxy
|
||||||
from stator import views as stator
|
from stator import views as stator
|
||||||
from users.views import activitypub, admin, auth, identity, settings
|
from users.views import activitypub, admin, auth, identity, report, settings
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", core.homepage),
|
path("", core.homepage),
|
||||||
|
@ -114,6 +114,16 @@ urlpatterns = [
|
||||||
admin.IdentityEdit.as_view(),
|
admin.IdentityEdit.as_view(),
|
||||||
name="admin_identity_edit",
|
name="admin_identity_edit",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"admin/reports/",
|
||||||
|
admin.ReportsRoot.as_view(),
|
||||||
|
name="admin_reports",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"admin/reports/<id>/",
|
||||||
|
admin.ReportView.as_view(),
|
||||||
|
name="admin_report_view",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"admin/invites/",
|
"admin/invites/",
|
||||||
admin.Invites.as_view(),
|
admin.Invites.as_view(),
|
||||||
|
@ -147,6 +157,7 @@ urlpatterns = [
|
||||||
path("@<handle>/inbox/", activitypub.Inbox.as_view()),
|
path("@<handle>/inbox/", activitypub.Inbox.as_view()),
|
||||||
path("@<handle>/action/", identity.ActionIdentity.as_view()),
|
path("@<handle>/action/", identity.ActionIdentity.as_view()),
|
||||||
path("@<handle>/rss/", identity.IdentityFeed()),
|
path("@<handle>/rss/", identity.IdentityFeed()),
|
||||||
|
path("@<handle>/report/", report.SubmitReport.as_view()),
|
||||||
# Posts
|
# Posts
|
||||||
path("compose/", compose.Compose.as_view(), name="compose"),
|
path("compose/", compose.Compose.as_view(), name="compose"),
|
||||||
path(
|
path(
|
||||||
|
@ -160,6 +171,7 @@ urlpatterns = [
|
||||||
path("@<handle>/posts/<int:post_id>/boost/", posts.Boost.as_view()),
|
path("@<handle>/posts/<int:post_id>/boost/", posts.Boost.as_view()),
|
||||||
path("@<handle>/posts/<int:post_id>/unboost/", posts.Boost.as_view(undo=True)),
|
path("@<handle>/posts/<int:post_id>/unboost/", posts.Boost.as_view(undo=True)),
|
||||||
path("@<handle>/posts/<int:post_id>/delete/", posts.Delete.as_view()),
|
path("@<handle>/posts/<int:post_id>/delete/", posts.Delete.as_view()),
|
||||||
|
path("@<handle>/posts/<int:post_id>/report/", report.SubmitReport.as_view()),
|
||||||
path("@<handle>/posts/<int:post_id>/edit/", compose.Compose.as_view()),
|
path("@<handle>/posts/<int:post_id>/edit/", compose.Compose.as_view()),
|
||||||
# Authentication
|
# Authentication
|
||||||
path("auth/login/", auth.Login.as_view(), name="login"),
|
path("auth/login/", auth.Login.as_view(), name="login"),
|
||||||
|
|
|
@ -37,6 +37,9 @@
|
||||||
<a href="{{ post.urls.view }}" role="menuitem">
|
<a href="{{ post.urls.view }}" role="menuitem">
|
||||||
<i class="fa-solid fa-comment"></i> View Post & Replies
|
<i class="fa-solid fa-comment"></i> View Post & Replies
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ post.urls.action_report }}" role="menuitem">
|
||||||
|
<i class="fa-solid fa-flag"></i> Report
|
||||||
|
</a>
|
||||||
{% if post.author == request.identity %}
|
{% if post.author == request.identity %}
|
||||||
<a href="{{ post.urls.action_edit }}" role="menuitem">
|
<a href="{{ post.urls.action_edit }}" role="menuitem">
|
||||||
<i class="fa-solid fa-pen-to-square"></i> Edit
|
<i class="fa-solid fa-pen-to-square"></i> Edit
|
||||||
|
|
84
templates/admin/report_view.html
Normal file
84
templates/admin/report_view.html
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
{% extends "settings/base.html" %}
|
||||||
|
|
||||||
|
{% block subtitle %}Report {{ report.pk }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form action="." method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<fieldset>
|
||||||
|
<legend>Report</legend>
|
||||||
|
<label>Report about</label>
|
||||||
|
{% if report.subject_post %}
|
||||||
|
{% include "activities/_mini_post.html" with post=report.subject_post %}
|
||||||
|
{% else %}
|
||||||
|
{% include "activities/_identity.html" with identity=report.subject_identity %}
|
||||||
|
{% endif %}
|
||||||
|
<label>Reported by</label>
|
||||||
|
{% if report.source_identity %}
|
||||||
|
{% include "activities/_identity.html" with identity=report.source_identity %}
|
||||||
|
{% else %}
|
||||||
|
<p>Remote server {{ report.source_domain.domain }}</p>
|
||||||
|
{% endif %}
|
||||||
|
<label>Complaint</label>
|
||||||
|
<p>{{ report.complaint|linebreaks }}</p>
|
||||||
|
{% if report.resolved %}
|
||||||
|
<label>Resolved</label>
|
||||||
|
<p>
|
||||||
|
{{ report.resolved|timesince }} ago by
|
||||||
|
<a href="{{ report.moderator.urls.view }}">{{ report.moderator.name_or_handle }}</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Moderator Notes</legend>
|
||||||
|
{% include "forms/_field.html" with field=form.notes %}
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Resolution Options</legend>
|
||||||
|
<table class="buttons">
|
||||||
|
<tr>
|
||||||
|
{% if report.resolved and report.valid %}
|
||||||
|
<th><button disabled="true">Resolve Valid</button></th>
|
||||||
|
<td>Report is already resolved as valid</td>
|
||||||
|
{% else %}
|
||||||
|
<th><button name="valid">Resolve Valid</button></th>
|
||||||
|
<td>Mark report against the identity but take no further action</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
{% if report.resolved and not report.valid %}
|
||||||
|
<th><button disabled="true">Resolve Invalid</button></th>
|
||||||
|
<td>Report is already resolved as invalid</td>
|
||||||
|
{% else %}
|
||||||
|
<th><button name="invalid">Resolve Invalid</button></th>
|
||||||
|
<td>Mark report as invalid and take no action</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
{% if report.subject_identity.limited %}
|
||||||
|
<th><button class="danger" disabled="true">Limit</button></th>
|
||||||
|
<td>User is already limited</td>
|
||||||
|
{% else %}
|
||||||
|
<th><button class="danger" name="limit">Limit</button></th>
|
||||||
|
<td>Make them less visible on this server</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
{% if report.subject_identity.blocked %}
|
||||||
|
<th><button class="danger" disabled="true">Block</button></th>
|
||||||
|
<td>User is already blocked</td>
|
||||||
|
{% else %}
|
||||||
|
<th><button class="danger" name="block">Block</button></th>
|
||||||
|
<td>Remove their existence entirely from this server</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</fieldset>
|
||||||
|
<div class="buttons">
|
||||||
|
<a href="{{ report.urls.admin }}" class="button secondary left">Back</a>
|
||||||
|
<a href="{{ report.subject_identity.urls.view }}" class="button secondary">View Profile</a>
|
||||||
|
<a href="{{ report.subject_identity.urls.admin_edit }}" class="button secondary">Identity Admin</a>
|
||||||
|
<button>Save Notes</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
43
templates/admin/reports.html
Normal file
43
templates/admin/reports.html
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
{% extends "settings/base.html" %}
|
||||||
|
{% load activity_tags %}
|
||||||
|
|
||||||
|
{% block subtitle %}Reports{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="view-options">
|
||||||
|
{% if all %}
|
||||||
|
<a href="." class="selected"><i class="fa-solid fa-check"></i> Show Resolved</a>
|
||||||
|
{% else %}
|
||||||
|
<a href=".?all=true"><i class="fa-solid fa-xmark"></i> Show Resolved</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<section class="icon-menu">
|
||||||
|
{% for report in page_obj %}
|
||||||
|
<a class="option" href="{{ report.urls.admin_view }}">
|
||||||
|
<img src="{{ report.subject_identity.local_icon_url.relative }}" class="icon" alt="Avatar for {{ report.subject_identity.name_or_handle }}">
|
||||||
|
<span class="handle">
|
||||||
|
{{ report.subject_identity.html_name_or_handle }}
|
||||||
|
{% if report.subject_post %}
|
||||||
|
(post {{ report.subject_post.pk }})
|
||||||
|
{% endif %}
|
||||||
|
<small>
|
||||||
|
{{ report.type|title }}
|
||||||
|
</small>
|
||||||
|
</span>
|
||||||
|
<time>{{ report.created|timedeltashort }} ago</time>
|
||||||
|
</a>
|
||||||
|
{% empty %}
|
||||||
|
<p class="option empty">
|
||||||
|
There are no {% if all %}reports yet{% else %}unresolved reports{% endif %}.
|
||||||
|
</p>
|
||||||
|
{% endfor %}
|
||||||
|
<div class="load-more">
|
||||||
|
{% if page_obj.has_previous %}
|
||||||
|
<a class="button" href=".?page={{ page_obj.previous_page_number }}{% if all %}&all=true{% endif %}">Previous Page</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if page_obj.has_next %}
|
||||||
|
<a class="button" href=".?page={{ page_obj.next_page_number }}{% if all %}&all=true{% endif %}">Next Page</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
|
@ -39,8 +39,8 @@
|
||||||
<a href="{% url "admin_hashtags" %}" {% if section == "hashtags" %}class="selected"{% endif %} title="Hashtags">
|
<a href="{% url "admin_hashtags" %}" {% if section == "hashtags" %}class="selected"{% endif %} title="Hashtags">
|
||||||
<i class="fa-solid fa-hashtag"></i> Hashtags
|
<i class="fa-solid fa-hashtag"></i> Hashtags
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url "admin_tuning" %}" {% if section == "tuning" %}class="selected"{% endif %} title="Tuning">
|
<a href="{% url "admin_reports" %}" {% if section == "reports" %}class="selected"{% endif %} title="Reports">
|
||||||
<i class="fa-solid fa-wrench"></i> Tuning
|
<i class="fa-solid fa-flag"></i> Reports
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url "admin_stator" %}" {% if section == "stator" %}class="selected"{% endif %} title="Stator">
|
<a href="{% url "admin_stator" %}" {% if section == "stator" %}class="selected"{% endif %} title="Stator">
|
||||||
<i class="fa-solid fa-clock-rotate-left"></i> Stator
|
<i class="fa-solid fa-clock-rotate-left"></i> Stator
|
||||||
|
|
27
templates/users/report.html
Normal file
27
templates/users/report.html
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Report{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form action="." method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<fieldset>
|
||||||
|
<legend>Report</legend>
|
||||||
|
<label>Reporting</label>
|
||||||
|
{% if post %}
|
||||||
|
{% include "activities/_mini_post.html" %}
|
||||||
|
{% else %}
|
||||||
|
{% include "activities/_identity.html" %}
|
||||||
|
{% endif %}
|
||||||
|
{% include "forms/_field.html" with field=form.type %}
|
||||||
|
{% include "forms/_field.html" with field=form.complaint %}
|
||||||
|
{% if not identity.local %}
|
||||||
|
{% include "forms/_field.html" with field=form.forward %}
|
||||||
|
{% endif %}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<button>Send Report</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
13
templates/users/report_sent.html
Normal file
13
templates/users/report_sent.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Report Sent{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form action="." method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<fieldset>
|
||||||
|
<legend>Report</legend>
|
||||||
|
<p>Your report has been sent.</p>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
|
@ -9,6 +9,7 @@ from users.models import (
|
||||||
InboxMessage,
|
InboxMessage,
|
||||||
Invite,
|
Invite,
|
||||||
PasswordReset,
|
PasswordReset,
|
||||||
|
Report,
|
||||||
User,
|
User,
|
||||||
UserEvent,
|
UserEvent,
|
||||||
)
|
)
|
||||||
|
@ -113,3 +114,8 @@ class InboxMessageAdmin(admin.ModelAdmin):
|
||||||
@admin.register(Invite)
|
@admin.register(Invite)
|
||||||
class InviteAdmin(admin.ModelAdmin):
|
class InviteAdmin(admin.ModelAdmin):
|
||||||
list_display = ["id", "created", "token", "note"]
|
list_display = ["id", "created", "token", "note"]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Report)
|
||||||
|
class ReportAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["id", "created", "resolved", "type", "subject_identity"]
|
||||||
|
|
119
users/migrations/0005_report.py
Normal file
119
users/migrations/0005_report.py
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
# Generated by Django 4.1.4 on 2022-12-17 20:38
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import stator.models
|
||||||
|
import users.models.report
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("activities", "0004_emoji_post_emojis"),
|
||||||
|
("users", "0004_identity_admin_notes_identity_restriction_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Report",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("state_ready", models.BooleanField(default=True)),
|
||||||
|
("state_changed", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("state_attempted", models.DateTimeField(blank=True, null=True)),
|
||||||
|
("state_locked_until", models.DateTimeField(blank=True, null=True)),
|
||||||
|
(
|
||||||
|
"state",
|
||||||
|
stator.models.StateField(
|
||||||
|
choices=[("new", "new"), ("sent", "sent")],
|
||||||
|
default="new",
|
||||||
|
graph=users.models.report.ReportStates,
|
||||||
|
max_length=100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"type",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("spam", "Spam"),
|
||||||
|
("hateful", "Hateful"),
|
||||||
|
("illegal", "Illegal"),
|
||||||
|
("remote", "Remote"),
|
||||||
|
("other", "Other"),
|
||||||
|
],
|
||||||
|
max_length=100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("complaint", models.TextField()),
|
||||||
|
("forward", models.BooleanField(default=False)),
|
||||||
|
("valid", models.BooleanField(null=True)),
|
||||||
|
("seen", models.DateTimeField(blank=True, null=True)),
|
||||||
|
("resolved", models.DateTimeField(blank=True, null=True)),
|
||||||
|
("notes", models.TextField(blank=True, null=True)),
|
||||||
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated", models.DateTimeField(auto_now=True)),
|
||||||
|
(
|
||||||
|
"moderator",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="moderated_reports",
|
||||||
|
to="users.identity",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"source_domain",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="filed_reports",
|
||||||
|
to="users.domain",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"source_identity",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="filed_reports",
|
||||||
|
to="users.identity",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"subject_identity",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="reports",
|
||||||
|
to="users.identity",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"subject_post",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="reports",
|
||||||
|
to="activities.post",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -5,6 +5,7 @@ from .identity import Identity, IdentityStates # noqa
|
||||||
from .inbox_message import InboxMessage, InboxMessageStates # noqa
|
from .inbox_message import InboxMessage, InboxMessageStates # noqa
|
||||||
from .invite import Invite # noqa
|
from .invite import Invite # noqa
|
||||||
from .password_reset import PasswordReset # noqa
|
from .password_reset import PasswordReset # noqa
|
||||||
|
from .report import Report # noqa
|
||||||
from .system_actor import SystemActor # noqa
|
from .system_actor import SystemActor # noqa
|
||||||
from .user import User # noqa
|
from .user import User # noqa
|
||||||
from .user_event import UserEvent # noqa
|
from .user_event import UserEvent # noqa
|
||||||
|
|
129
users/models/report.py
Normal file
129
users/models/report.py
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
import httpx
|
||||||
|
import urlman
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from core.ld import canonicalise
|
||||||
|
from stator.models import State, StateField, StateGraph, StatorModel
|
||||||
|
from users.models import SystemActor
|
||||||
|
|
||||||
|
|
||||||
|
class ReportStates(StateGraph):
|
||||||
|
new = State(try_interval=600)
|
||||||
|
sent = State()
|
||||||
|
|
||||||
|
new.transitions_to(sent)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def handle_new(cls, instance: "Report"):
|
||||||
|
"""
|
||||||
|
Sends the report to the remote server if we need to
|
||||||
|
"""
|
||||||
|
report = await instance.afetch_full()
|
||||||
|
if report.forward and not report.subject_identity.domain.local:
|
||||||
|
system_actor = SystemActor()
|
||||||
|
try:
|
||||||
|
await system_actor.signed_request(
|
||||||
|
method="post",
|
||||||
|
uri=report.subject_identity.inbox_uri,
|
||||||
|
body=canonicalise(report.to_ap()),
|
||||||
|
)
|
||||||
|
except httpx.RequestError:
|
||||||
|
return
|
||||||
|
return cls.sent
|
||||||
|
|
||||||
|
|
||||||
|
class Report(StatorModel):
|
||||||
|
"""
|
||||||
|
A complaint about a user or post.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Types(models.TextChoices):
|
||||||
|
spam = "spam"
|
||||||
|
hateful = "hateful"
|
||||||
|
illegal = "illegal"
|
||||||
|
remote = "remote"
|
||||||
|
other = "other"
|
||||||
|
|
||||||
|
state = StateField(ReportStates)
|
||||||
|
|
||||||
|
subject_identity = models.ForeignKey(
|
||||||
|
"users.Identity",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
related_name="reports",
|
||||||
|
)
|
||||||
|
subject_post = models.ForeignKey(
|
||||||
|
"activities.Post",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
related_name="reports",
|
||||||
|
)
|
||||||
|
|
||||||
|
source_identity = models.ForeignKey(
|
||||||
|
"users.Identity",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
related_name="filed_reports",
|
||||||
|
)
|
||||||
|
source_domain = models.ForeignKey(
|
||||||
|
"users.Domain",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
related_name="filed_reports",
|
||||||
|
)
|
||||||
|
|
||||||
|
moderator = models.ForeignKey(
|
||||||
|
"users.Identity",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
related_name="moderated_reports",
|
||||||
|
)
|
||||||
|
|
||||||
|
type = models.CharField(max_length=100, choices=Types.choices)
|
||||||
|
complaint = models.TextField()
|
||||||
|
forward = models.BooleanField(default=False)
|
||||||
|
valid = models.BooleanField(null=True)
|
||||||
|
|
||||||
|
seen = models.DateTimeField(blank=True, null=True)
|
||||||
|
resolved = models.DateTimeField(blank=True, null=True)
|
||||||
|
notes = models.TextField(blank=True, null=True)
|
||||||
|
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class urls(urlman.Urls):
|
||||||
|
admin = "/admin/reports/"
|
||||||
|
admin_view = "{admin}{self.pk}/"
|
||||||
|
|
||||||
|
### ActivityPub ###
|
||||||
|
|
||||||
|
async def afetch_full(self) -> "Report":
|
||||||
|
return await Report.objects.select_related(
|
||||||
|
"source_identity",
|
||||||
|
"source_domain",
|
||||||
|
"subject_identity__domain",
|
||||||
|
"subject_identity",
|
||||||
|
"subject_post",
|
||||||
|
).aget(pk=self.pk)
|
||||||
|
|
||||||
|
def to_ap(self):
|
||||||
|
system_actor = SystemActor()
|
||||||
|
if self.subject_post:
|
||||||
|
objects = [
|
||||||
|
self.subject_post.object_uri,
|
||||||
|
self.subject_identity.actor_uri,
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
objects = self.subject_identity.actor_uri
|
||||||
|
return {
|
||||||
|
"id": f"https://{self.source_domain.uri_domain}/reports/{self.id}/",
|
||||||
|
"type": "Flag",
|
||||||
|
"actor": system_actor.actor_uri,
|
||||||
|
"object": objects,
|
||||||
|
"content": self.complaint,
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ from users.views.admin.hashtags import ( # noqa
|
||||||
Hashtags,
|
Hashtags,
|
||||||
)
|
)
|
||||||
from users.views.admin.identities import IdentitiesRoot, IdentityEdit # noqa
|
from users.views.admin.identities import IdentitiesRoot, IdentityEdit # noqa
|
||||||
|
from users.views.admin.reports import ReportsRoot, ReportView # noqa
|
||||||
from users.views.admin.settings import ( # noqa
|
from users.views.admin.settings import ( # noqa
|
||||||
BasicSettings,
|
BasicSettings,
|
||||||
PoliciesSettings,
|
PoliciesSettings,
|
||||||
|
|
80
users/views/admin/reports.py
Normal file
80
users/views/admin/reports.py
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
from django import forms
|
||||||
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.generic import FormView, ListView
|
||||||
|
|
||||||
|
from users.decorators import admin_required
|
||||||
|
from users.models import Identity, Report
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(admin_required, name="dispatch")
|
||||||
|
class ReportsRoot(ListView):
|
||||||
|
|
||||||
|
template_name = "admin/reports.html"
|
||||||
|
paginate_by = 30
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
self.query = request.GET.get("query")
|
||||||
|
self.all = request.GET.get("all")
|
||||||
|
self.extra_context = {
|
||||||
|
"section": "reports",
|
||||||
|
"all": self.all,
|
||||||
|
}
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
reports = Report.objects.select_related(
|
||||||
|
"subject_post", "subject_identity"
|
||||||
|
).order_by("created")
|
||||||
|
if not self.all:
|
||||||
|
reports = reports.filter(resolved__isnull=True)
|
||||||
|
return reports
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(admin_required, name="dispatch")
|
||||||
|
class ReportView(FormView):
|
||||||
|
|
||||||
|
template_name = "admin/report_view.html"
|
||||||
|
extra_context = {
|
||||||
|
"section": "reports",
|
||||||
|
}
|
||||||
|
|
||||||
|
class form_class(forms.Form):
|
||||||
|
notes = forms.CharField(widget=forms.Textarea, required=False)
|
||||||
|
|
||||||
|
def dispatch(self, request, id, *args, **kwargs):
|
||||||
|
self.report = get_object_or_404(Report, id=id)
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
if "limit" in request.POST:
|
||||||
|
self.report.subject_identity.restriction = Identity.Restriction.limited
|
||||||
|
self.report.subject_identity.save()
|
||||||
|
if "block" in request.POST:
|
||||||
|
self.report.subject_identity.restriction = Identity.Restriction.blocked
|
||||||
|
self.report.subject_identity.save()
|
||||||
|
if "valid" in request.POST:
|
||||||
|
self.report.resolved = timezone.now()
|
||||||
|
self.report.valid = True
|
||||||
|
self.report.moderator = self.request.identity
|
||||||
|
self.report.save()
|
||||||
|
if "invalid" in request.POST:
|
||||||
|
self.report.resolved = timezone.now()
|
||||||
|
self.report.valid = False
|
||||||
|
self.report.moderator = self.request.identity
|
||||||
|
self.report.save()
|
||||||
|
return super().post(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
return {"notes": self.report.notes}
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
self.report.notes = form.cleaned_data["notes"]
|
||||||
|
self.report.save()
|
||||||
|
return redirect(".")
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context["report"] = self.report
|
||||||
|
return context
|
76
users/views/report.py
Normal file
76
users/views/report.py
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
from django import forms
|
||||||
|
from django.shortcuts import get_object_or_404, render
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.generic import FormView
|
||||||
|
|
||||||
|
from users.decorators import identity_required
|
||||||
|
from users.models import Report
|
||||||
|
from users.shortcuts import by_handle_or_404
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(identity_required, name="dispatch")
|
||||||
|
class SubmitReport(FormView):
|
||||||
|
"""
|
||||||
|
Submits a report on a user or a post
|
||||||
|
"""
|
||||||
|
|
||||||
|
template_name = "users/report.html"
|
||||||
|
|
||||||
|
class form_class(forms.Form):
|
||||||
|
type = forms.ChoiceField(
|
||||||
|
choices=[
|
||||||
|
("", "------"),
|
||||||
|
("spam", "Spam or inappropriate advertising"),
|
||||||
|
("hateful", "Hateful, abusive, or violent speech"),
|
||||||
|
("other", "Something else"),
|
||||||
|
],
|
||||||
|
label="Why are you reporting this?",
|
||||||
|
)
|
||||||
|
|
||||||
|
complaint = forms.CharField(
|
||||||
|
widget=forms.Textarea,
|
||||||
|
help_text="Please describe why you think this should be removed",
|
||||||
|
)
|
||||||
|
|
||||||
|
forward = forms.BooleanField(
|
||||||
|
widget=forms.Select(
|
||||||
|
choices=[
|
||||||
|
(False, "Do not send to other server"),
|
||||||
|
(True, "Send to other server"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
help_text="Should we also send an anonymous copy of this to their server?",
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def dispatch(self, request, handle, post_id=None):
|
||||||
|
self.identity = by_handle_or_404(self.request, handle, local=False)
|
||||||
|
if post_id:
|
||||||
|
self.post_obj = get_object_or_404(self.identity.posts, pk=post_id)
|
||||||
|
else:
|
||||||
|
self.post_obj = None
|
||||||
|
return super().dispatch(request)
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
# Create the report
|
||||||
|
report = Report.objects.create(
|
||||||
|
type=form.cleaned_data["type"],
|
||||||
|
complaint=form.cleaned_data["complaint"],
|
||||||
|
subject_identity=self.identity,
|
||||||
|
subject_post=self.post_obj,
|
||||||
|
source_identity=self.request.identity,
|
||||||
|
source_domain=self.request.identity.domain,
|
||||||
|
forward=form.cleaned_data.get("forward", False),
|
||||||
|
)
|
||||||
|
# Show a thanks page
|
||||||
|
return render(
|
||||||
|
self.request,
|
||||||
|
"users/report_sent.html",
|
||||||
|
{"report": report},
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
context = super().get_context_data(*args, **kwargs)
|
||||||
|
context["identity"] = self.identity
|
||||||
|
context["post"] = self.post_obj
|
||||||
|
return context
|
Loading…
Reference in a new issue