Identity admin/moderation
This commit is contained in:
parent
c588567c86
commit
12567f6891
@ -1,6 +1,6 @@
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db import models
|
||||
from django.http import JsonResponse
|
||||
from django.http import Http404, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.vary import vary_on_headers
|
||||
@ -10,6 +10,7 @@ from activities.models import Post, PostInteraction, PostStates
|
||||
from core.decorators import cache_page_by_ap_json
|
||||
from core.ld import canonicalise
|
||||
from users.decorators import identity_required
|
||||
from users.models import Identity
|
||||
from users.shortcuts import by_handle_or_404
|
||||
|
||||
|
||||
@ -23,6 +24,8 @@ class Individual(TemplateView):
|
||||
|
||||
def get(self, request, handle, post_id):
|
||||
self.identity = by_handle_or_404(self.request, handle, local=False)
|
||||
if self.identity.blocked:
|
||||
raise Http404("Blocked user")
|
||||
self.post_obj = get_object_or_404(self.identity.posts, pk=post_id)
|
||||
# If they're coming in looking for JSON, they want the actor
|
||||
if request.ap_json:
|
||||
@ -66,6 +69,7 @@ class Individual(TemplateView):
|
||||
),
|
||||
in_reply_to=self.post_obj.object_uri,
|
||||
)
|
||||
.exclude(author__restriction=Identity.Restriction.blocked)
|
||||
.distinct()
|
||||
.select_related("author__domain")
|
||||
.prefetch_related("emojis")
|
||||
|
@ -7,6 +7,7 @@ from django.views.generic import FormView, ListView
|
||||
from activities.models import Hashtag, Post, PostInteraction, TimelineEvent
|
||||
from core.decorators import cache_page
|
||||
from users.decorators import identity_required
|
||||
from users.models import Identity
|
||||
|
||||
from .compose import Compose
|
||||
|
||||
@ -75,6 +76,7 @@ class Tag(ListView):
|
||||
def get_queryset(self):
|
||||
return (
|
||||
Post.objects.public()
|
||||
.filter(author__restriction=Identity.Restriction.none)
|
||||
.tagged_with(self.hashtag)
|
||||
.select_related("author")
|
||||
.prefetch_related("attachments", "mentions")
|
||||
@ -105,6 +107,7 @@ class Local(ListView):
|
||||
def get_queryset(self):
|
||||
return (
|
||||
Post.objects.local_public()
|
||||
.filter(author__restriction=Identity.Restriction.none)
|
||||
.select_related("author", "author__domain")
|
||||
.prefetch_related("attachments", "mentions", "emojis")
|
||||
.order_by("-created")
|
||||
@ -133,6 +136,7 @@ class Federated(ListView):
|
||||
Post.objects.filter(
|
||||
visibility=Post.Visibilities.public, in_reply_to__isnull=True
|
||||
)
|
||||
.filter(author__restriction=Identity.Restriction.none)
|
||||
.select_related("author", "author__domain")
|
||||
.prefetch_related("attachments", "mentions", "emojis")
|
||||
.order_by("-created")
|
||||
|
@ -48,7 +48,9 @@ def account_relationships(request):
|
||||
@api_router.get("/v1/accounts/{id}", response=schemas.Account)
|
||||
@identity_required
|
||||
def account(request, id: str):
|
||||
identity = get_object_or_404(Identity, pk=id)
|
||||
identity = get_object_or_404(
|
||||
Identity.objects.exclude(restriction=Identity.Restriction.blocked), pk=id
|
||||
)
|
||||
return identity.to_mastodon_json()
|
||||
|
||||
|
||||
@ -67,7 +69,9 @@ def account_statuses(
|
||||
min_id: str | None = None,
|
||||
limit: int = 20,
|
||||
):
|
||||
identity = get_object_or_404(Identity, pk=id)
|
||||
identity = get_object_or_404(
|
||||
Identity.objects.exclude(restriction=Identity.Restriction.blocked), pk=id
|
||||
)
|
||||
queryset = (
|
||||
identity.posts.not_hidden()
|
||||
.unlisted(include_replies=not exclude_replies)
|
||||
|
@ -3,6 +3,7 @@ from api import schemas
|
||||
from api.decorators import identity_required
|
||||
from api.pagination import MastodonPaginator
|
||||
from api.views.base import api_router
|
||||
from users.models import Identity
|
||||
|
||||
|
||||
@api_router.get("/v1/timelines/home", response=list[schemas.Status])
|
||||
@ -52,6 +53,7 @@ def public(
|
||||
):
|
||||
queryset = (
|
||||
Post.objects.public()
|
||||
.filter(author__restriction=Identity.Restriction.none)
|
||||
.select_related("author")
|
||||
.prefetch_related("attachments")
|
||||
.order_by("-created")
|
||||
@ -90,6 +92,7 @@ def hashtag(
|
||||
limit = 40
|
||||
queryset = (
|
||||
Post.objects.public()
|
||||
.filter(author__restriction=Identity.Restriction.none)
|
||||
.tagged_with(hashtag)
|
||||
.select_related("author")
|
||||
.prefetch_related("attachments")
|
||||
|
@ -18,6 +18,7 @@ in alpha. For more information about Takahē, see
|
||||
features
|
||||
contributing
|
||||
domains
|
||||
moderation
|
||||
stator
|
||||
tuning
|
||||
releases/index
|
||||
|
99
docs/moderation.rst
Normal file
99
docs/moderation.rst
Normal file
@ -0,0 +1,99 @@
|
||||
Moderation
|
||||
==========
|
||||
|
||||
As a server admin, you have both identity-level and server-level moderation
|
||||
options at your disposal.
|
||||
|
||||
|
||||
Identities
|
||||
----------
|
||||
|
||||
Identities, known as Accounts in Mastodon, have their own handle
|
||||
(like ``@takahe@jointakahe.org``), and are generally what people think of as
|
||||
"users".
|
||||
|
||||
Takahē distinguishes between the two - for us, a User is a set of login
|
||||
credentials, while an Identity is the public-facing identity people use to
|
||||
post. A user can have multiple identities, and an identity can be shared
|
||||
across multiple users (for example, a brand account that five people can
|
||||
post from).
|
||||
|
||||
You can moderate both local and remote identities, but bear in mind that any
|
||||
moderation actions on *remote identities* are local to your server only;
|
||||
they will not propagate over to other servers.
|
||||
|
||||
Identity moderation actions are available in the "Identities" admin area.
|
||||
|
||||
|
||||
Limiting
|
||||
~~~~~~~~
|
||||
|
||||
Limiting an identity prevents its posts from appearing in the Public and
|
||||
Federated timelines; they will, however, still appear in the timelines of
|
||||
people who follow them, be able to notify other people via mentions, and their
|
||||
replies will appear in conversation threads.
|
||||
|
||||
You can limit both local and remote identities. Limiting is reversible,
|
||||
and encouraged as a way to remove some visibility if you don't want a full block.
|
||||
|
||||
|
||||
Blocking
|
||||
~~~~~~~~
|
||||
|
||||
Blocking an identity erases its existence from your server. Its posts will
|
||||
not appear anywhere, no mentions from it will come through, and Takahē will
|
||||
actively discard all incoming information from it as soon as it is received.
|
||||
|
||||
If you block a local identity, you are freezing the account and erasing it
|
||||
from the Fediverse. Takahē will still accept inbound notifications for it,
|
||||
but if any servers ask if it exists, it will deny its existence. Users trying
|
||||
to log into that identity will be denied access.
|
||||
|
||||
If you block a remote identity, you are almost erasing it from existence
|
||||
from your server's users. Users will not be able to follow it or see posts
|
||||
from it; they will, however, be able to mention it in outgoing posts.
|
||||
|
||||
Blocking is reversible; however, you will lose data intended for the account
|
||||
for the duration it is blocked for. If you leave a local account blocked for
|
||||
too long, other servers will decide it has totally vanished and stop their
|
||||
users following it.
|
||||
|
||||
|
||||
Servers
|
||||
-------
|
||||
|
||||
If your problem is not with an individual identity/account but with an entire
|
||||
server - be it very poorly run or actively malicious - you can instead
|
||||
choose to block the entire server ("defederate").
|
||||
|
||||
This is accomplished via the "Federation" admin area. Search and select the
|
||||
domain you want, and then set it to blocked.
|
||||
|
||||
While a domain is blocked, Takahē will actively drop all inbound messages
|
||||
from it. Blocking is reversible, but you will lose all inbound data from the
|
||||
server during the blocking period.
|
||||
|
||||
|
||||
Defederating from Takahē
|
||||
------------------------
|
||||
|
||||
Takahē is unusual in the Fediverse in that it's possible to have it claim to be
|
||||
multiple different domains at once; this extends to the way it speaks to
|
||||
other servers, and means you cannot easily block an entire Takahē installation at once.
|
||||
|
||||
If you wish to block a Takahē server, either from Takahē or any other Fediverse
|
||||
server that supports defederation, you may choose to either block a single
|
||||
domain as normal, or you may want to block the entire server.
|
||||
|
||||
Takahē sends all actor messages from identities based on the domain they are
|
||||
part of, but uses a single System Actor for all GET requests to retrieve
|
||||
identity and post information. To properly defederate a Takahē server, you
|
||||
need to:
|
||||
|
||||
* Block all domains you know it has identities on
|
||||
* Block the domain of the System Actor (visible at the ``/actor/`` URL)
|
||||
|
||||
If you are having trouble blocking a Takahē server due to this, we apologise;
|
||||
this is the nature of the underlying protocol. If you find a server that breaks
|
||||
our `Code of Conduct <https://jointakahe.org/conduct/>`_, please let us know
|
||||
at conduct@jointakahe.org and we will do our best to not give them any support.
|
@ -307,6 +307,14 @@ nav a i {
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.left-column h1 small {
|
||||
font-size: 60%;
|
||||
color: var(--color-text-dull);
|
||||
display: block;
|
||||
margin: -10px 0 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.left-column h2 {
|
||||
margin: 10px 0 10px 0;
|
||||
}
|
||||
@ -642,10 +650,15 @@ form .uploaded-image .buttons {
|
||||
}
|
||||
|
||||
form .buttons {
|
||||
clear: both;
|
||||
text-align: right;
|
||||
margin: -20px 0 15px 0;
|
||||
}
|
||||
|
||||
form .buttons:nth-of-type(2) {
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
form p+.buttons,
|
||||
form fieldset .buttons {
|
||||
margin-top: 0;
|
||||
@ -794,14 +807,15 @@ h1.identity small {
|
||||
|
||||
table.metadata {
|
||||
margin: -10px 0 0 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table.metadata td {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table.metadata td.name {
|
||||
padding-right: 10px;
|
||||
table.metadata th {
|
||||
padding: 0 10px 0 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
@ -106,9 +106,14 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"admin/identities/",
|
||||
admin.Identities.as_view(),
|
||||
admin.IdentitiesRoot.as_view(),
|
||||
name="admin_identities",
|
||||
),
|
||||
path(
|
||||
"admin/identities/<id>/",
|
||||
admin.IdentityEdit.as_view(),
|
||||
name="admin_identity_edit",
|
||||
),
|
||||
path(
|
||||
"admin/invites/",
|
||||
admin.Invites.as_view(),
|
||||
|
@ -3,7 +3,50 @@
|
||||
{% block subtitle %}Identities{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<p>
|
||||
Please use the <a href="/djadmin/users/identity/">Django Admin</a> for now.
|
||||
</p>
|
||||
<form action="." class="search">
|
||||
<input type="search" name="query" value="{{ query }}" placeholder="Search by name/username">
|
||||
{% if local_only %}
|
||||
<input type="hidden" name="local_only" value="true">
|
||||
{% endif %}
|
||||
<button><i class="fa-solid fa-search"></i></button>
|
||||
</form>
|
||||
<div class="view-options">
|
||||
{% if local_only %}
|
||||
<a href=".?{% if query %}query={{ query }}{% endif %}" class="selected"><i class="fa-solid fa-check"></i> Local Only</a>
|
||||
{% else %}
|
||||
<a href=".?local_only=true{% if query %}&query={{ query }}{% endif %}"><i class="fa-solid fa-xmark"></i> Local Only</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<section class="icon-menu">
|
||||
{% for identity in page_obj %}
|
||||
<a class="option" href="{{ identity.urls.admin_edit }}">
|
||||
<img src="{{ identity.local_icon_url.relative }}" class="icon" alt="Avatar for {{ identity.name_or_handle }}">
|
||||
<span class="handle">
|
||||
{{ identity.html_name_or_handle }}
|
||||
<small>
|
||||
{{ identity.handle }}
|
||||
</small>
|
||||
</span>
|
||||
{% if identity.banned %}
|
||||
<span class="pill bad">Banned</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% empty %}
|
||||
<p class="option empty">
|
||||
{% if query %}
|
||||
No identities match your query.
|
||||
{% else %}
|
||||
There are no identities yet.
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endfor %}
|
||||
<div class="load-more">
|
||||
{% if page_obj.has_previous %}
|
||||
<a class="button" href=".?page={{ page_obj.previous_page_number }}{% if local_only %}&local_only=true{% endif %}{% if query %}&query={{ query }}{% endif %}">Previous Page</a>
|
||||
{% endif %}
|
||||
{% if page_obj.has_next %}
|
||||
<a class="button" href=".?page={{ page_obj.next_page_number }}{% if local_only %}&local_only=true{% endif %}{% if query %}&query={{ query }}{% endif %}">Next Page</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
123
templates/admin/identity_edit.html
Normal file
123
templates/admin/identity_edit.html
Normal file
@ -0,0 +1,123 @@
|
||||
{% extends "settings/base.html" %}
|
||||
|
||||
{% block subtitle %}{{ identity.name_or_handle }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ identity.html_name_or_handle }} <small>{{ identity.handle }}</small></h1>
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
<legend>Stats</legend>
|
||||
<table class="metadata">
|
||||
<tr>
|
||||
<th>Status</td>
|
||||
<td>
|
||||
{% if identity.limited %}
|
||||
Limited
|
||||
{% elif identity.blocked %}
|
||||
Blocked
|
||||
{% else %}
|
||||
Normal
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if identity.local %}
|
||||
<tr>
|
||||
<th>Type</td>
|
||||
<td>Local Identity</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Followers</td>
|
||||
<td>{{ identity.inbound_follows.count }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Following</td>
|
||||
<td>{{ identity.outbound_follows.count }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<th>Type</td>
|
||||
<td>Remote Identity</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Local Followers</td>
|
||||
<td>{{ identity.inbound_follows.count }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Following Locals</td>
|
||||
<td>{{ identity.outbound_follows.count }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th>Posts</td>
|
||||
<td>{{ identity.posts.count }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>First Seen</td>
|
||||
<td>{{ identity.created|timesince }} ago</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
{% if identity.local %}
|
||||
<fieldset>
|
||||
<legend>Users</legend>
|
||||
<p>
|
||||
{% for user in identity.users.all %}
|
||||
<a href="{{ user.urls.admin_edit }}">{{ user.email }}</a>{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
<fieldset>
|
||||
<legend>Technical</legend>
|
||||
<table class="metadata">
|
||||
{% if not identity.local %}
|
||||
<tr>
|
||||
<th>Last Fetched</td>
|
||||
<td>{{ identity.fetched|timesince }} ago</td>
|
||||
</tr>
|
||||
{% if identity.state == "outdated" %}
|
||||
<tr>
|
||||
<th>Attempting Fetch Since</td>
|
||||
<td>{{ identity.state_changed|timesince }} ago</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th>Actor URI</td>
|
||||
<td>{{ identity.actor_uri }}</td>
|
||||
</tr>
|
||||
{% if not identity.local %}
|
||||
<tr>
|
||||
<th>Inbox URI</td>
|
||||
<td>{{ identity.inbox_uri }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Admin Notes</legend>
|
||||
{% include "forms/_field.html" with field=form.notes %}
|
||||
</fieldset>
|
||||
<div class="buttons">
|
||||
{% if not identity.local %}
|
||||
<button class="left" name="fetch">Force Fetch</a>
|
||||
{% endif %}
|
||||
{% if identity.limited %}
|
||||
<button class="left delete" name="unlimit">Unlimit</a>
|
||||
{% else %}
|
||||
<button class="left delete" name="limit">Limit</a>
|
||||
{% endif %}
|
||||
{% if identity.blocked %}
|
||||
<button class="left delete" name="unblock">Unblock</a>
|
||||
{% else %}
|
||||
<button class="left delete" name="block">Block</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<a href="{{ identity.urls.admin }}" class="button secondary left">Back</a>
|
||||
<a href="{{ identity.urls.view }}" class="button secondary">View Profile</a>
|
||||
<button>Save Notes</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
@ -6,8 +6,20 @@
|
||||
{% for model, stats in model_stats.items %}
|
||||
<fieldset>
|
||||
<legend>{{ model }}</legend>
|
||||
<p><b>Pending:</b> {{ stats.most_recent_queued }}</p>
|
||||
<p><b>Processed today:</b> {{ stats.most_recent_handled.1 }}</p>
|
||||
<table class="metadata">
|
||||
<tr>
|
||||
<th>Pending</td>
|
||||
<td>{{ stats.most_recent_queued }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Processed today</td>
|
||||
<td>{{ stats.most_recent_handled.1 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>This month</td>
|
||||
<td>{{ stats.most_recent_handled.2 }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% extends "settings/base.html" %}
|
||||
|
||||
{% block subtitle %}{{ user.email }}{% endblock %}
|
||||
{% block subtitle %}{{ editing_user.email }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ editing_user.email }}</h1>
|
||||
|
@ -66,11 +66,11 @@
|
||||
<table class="metadata">
|
||||
{% for entry in identity.safe_metadata %}
|
||||
<tr>
|
||||
<td class="name">{{ entry.name }}</td>
|
||||
<td class="value">{{ entry.value }}</td>
|
||||
<th>{{ entry.name }}</td>
|
||||
<td>{{ entry.value }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% if identity.config_identity.visible_follows %}
|
||||
|
@ -0,0 +1,30 @@
|
||||
# Generated by Django 4.1.4 on 2022-12-17 01:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("users", "0003_identity_followers_etc"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="identity",
|
||||
name="admin_notes",
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="identity",
|
||||
name="restriction",
|
||||
field=models.IntegerField(
|
||||
choices=[(0, "None"), (1, "Limited"), (2, "Blocked")], default=0
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="identity",
|
||||
name="sensitive",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
@ -55,6 +55,11 @@ class Identity(StatorModel):
|
||||
Represents both local and remote Fediverse identities (actors)
|
||||
"""
|
||||
|
||||
class Restriction(models.IntegerChoices):
|
||||
none = 0
|
||||
limited = 1
|
||||
blocked = 2
|
||||
|
||||
# The Actor URI is essentially also a PK - we keep the default numeric
|
||||
# one around as well for making nice URLs etc.
|
||||
actor_uri = models.CharField(max_length=500, unique=True)
|
||||
@ -105,6 +110,13 @@ class Identity(StatorModel):
|
||||
# Should be a list of object URIs (we don't want a full M2M here)
|
||||
pinned = models.JSONField(blank=True, null=True)
|
||||
|
||||
# Admin-only moderation fields
|
||||
sensitive = models.BooleanField(default=False)
|
||||
restriction = models.IntegerField(
|
||||
choices=Restriction.choices, default=Restriction.none
|
||||
)
|
||||
admin_notes = models.TextField(null=True, blank=True)
|
||||
|
||||
private_key = models.TextField(null=True, blank=True)
|
||||
public_key = models.TextField(null=True, blank=True)
|
||||
public_key_id = models.TextField(null=True, blank=True)
|
||||
@ -124,6 +136,8 @@ class Identity(StatorModel):
|
||||
view = "/@{self.username}@{self.domain_id}/"
|
||||
action = "{view}action/"
|
||||
activate = "{view}activate/"
|
||||
admin = "/admin/identities/"
|
||||
admin_edit = "{admin}{self.pk}/"
|
||||
|
||||
def get_scheme(self, url):
|
||||
return "https"
|
||||
@ -197,9 +211,16 @@ class Identity(StatorModel):
|
||||
domain = domain.lower()
|
||||
try:
|
||||
if local:
|
||||
return cls.objects.get(username=username, domain_id=domain, local=True)
|
||||
return cls.objects.get(
|
||||
username=username,
|
||||
domain_id=domain,
|
||||
local=True,
|
||||
)
|
||||
else:
|
||||
return cls.objects.get(username=username, domain_id=domain)
|
||||
return cls.objects.get(
|
||||
username=username,
|
||||
domain_id=domain,
|
||||
)
|
||||
except cls.DoesNotExist:
|
||||
if fetch and not local:
|
||||
actor_uri, handle = async_to_sync(cls.fetch_webfinger)(
|
||||
@ -277,6 +298,14 @@ class Identity(StatorModel):
|
||||
# TODO: Setting
|
||||
return self.data_age > 60 * 24 * 24
|
||||
|
||||
@property
|
||||
def blocked(self) -> bool:
|
||||
return self.restriction == self.Restriction.blocked
|
||||
|
||||
@property
|
||||
def limited(self) -> bool:
|
||||
return self.restriction == self.Restriction.limited
|
||||
|
||||
### ActivityPub (outbound) ###
|
||||
|
||||
def to_ap(self):
|
||||
|
@ -31,4 +31,6 @@ def by_handle_or_404(request, handle, local=True, fetch=False) -> Identity:
|
||||
)
|
||||
if identity is None:
|
||||
raise Http404(f"No identity for handle {handle}")
|
||||
if identity.blocked:
|
||||
raise Http404("Blocked user")
|
||||
return identity
|
||||
|
@ -165,11 +165,12 @@ class Inbox(View):
|
||||
f"Inbox error: cannot fetch actor {document['actor']}"
|
||||
)
|
||||
return HttpResponseBadRequest("Cannot retrieve actor")
|
||||
# See if it's from a blocked domain
|
||||
if identity.domain.blocked:
|
||||
|
||||
# See if it's from a blocked user or domain
|
||||
if identity.blocked or identity.domain.blocked:
|
||||
# I love to lie! Throw it away!
|
||||
exceptions.capture_message(
|
||||
f"Inbox: Discarded message from {identity.domain}"
|
||||
f"Inbox: Discarded message from {identity.actor_uri}"
|
||||
)
|
||||
return HttpResponse(status=202)
|
||||
|
||||
@ -185,6 +186,7 @@ class Inbox(View):
|
||||
except VerificationError:
|
||||
exceptions.capture_message("Inbox error: Bad LD signature")
|
||||
return HttpResponseUnauthorized("Bad signature")
|
||||
|
||||
# Otherwise, verify against the header (assuming it's the same actor)
|
||||
else:
|
||||
try:
|
||||
@ -200,6 +202,7 @@ class Inbox(View):
|
||||
except VerificationError:
|
||||
exceptions.capture_message("Inbox error: Bad HTTP signature")
|
||||
return HttpResponseUnauthorized("Bad signature")
|
||||
|
||||
# Hand off the item to be processed by the queue
|
||||
InboxMessage.objects.create(message=document)
|
||||
return HttpResponse(status=202)
|
||||
|
@ -1,9 +1,8 @@
|
||||
from django import forms
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.generic import FormView, RedirectView, TemplateView
|
||||
from django.views.generic import FormView, RedirectView
|
||||
|
||||
from users.decorators import admin_required
|
||||
from users.models import Identity
|
||||
from users.views.admin.domains import ( # noqa
|
||||
DomainCreate,
|
||||
DomainDelete,
|
||||
@ -17,6 +16,7 @@ from users.views.admin.hashtags import ( # noqa
|
||||
HashtagEdit,
|
||||
Hashtags,
|
||||
)
|
||||
from users.views.admin.identities import IdentitiesRoot, IdentityEdit # noqa
|
||||
from users.views.admin.settings import ( # noqa
|
||||
BasicSettings,
|
||||
PoliciesSettings,
|
||||
@ -31,18 +31,6 @@ class AdminRoot(RedirectView):
|
||||
pattern_name = "admin_basic"
|
||||
|
||||
|
||||
@method_decorator(admin_required, name="dispatch")
|
||||
class Identities(TemplateView):
|
||||
|
||||
template_name = "admin/identities.html"
|
||||
|
||||
def get_context_data(self):
|
||||
return {
|
||||
"identities": Identity.objects.order_by("username"),
|
||||
"section": "identities",
|
||||
}
|
||||
|
||||
|
||||
@method_decorator(admin_required, name="dispatch")
|
||||
class Invites(FormView):
|
||||
|
||||
|
90
users/views/admin/identities.py
Normal file
90
users/views/admin/identities.py
Normal file
@ -0,0 +1,90 @@
|
||||
from django import forms
|
||||
from django.db import models
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
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, IdentityStates
|
||||
|
||||
|
||||
@method_decorator(admin_required, name="dispatch")
|
||||
class IdentitiesRoot(ListView):
|
||||
|
||||
template_name = "admin/identities.html"
|
||||
paginate_by = 30
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.query = request.GET.get("query")
|
||||
self.local_only = request.GET.get("local_only")
|
||||
self.extra_context = {
|
||||
"section": "identities",
|
||||
"query": self.query or "",
|
||||
"local_only": self.local_only,
|
||||
}
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
identities = Identity.objects.annotate(
|
||||
num_users=models.Count("users")
|
||||
).order_by("created")
|
||||
if self.local_only:
|
||||
identities = identities.filter(local=True)
|
||||
if self.query:
|
||||
query = self.query.lower().strip().lstrip("@")
|
||||
if "@" in query:
|
||||
username, domain = query.split("@", 1)
|
||||
identities = identities.filter(
|
||||
username=username,
|
||||
domain__domain__istartswith=domain,
|
||||
)
|
||||
else:
|
||||
identities = identities.filter(
|
||||
models.Q(username__icontains=self.query)
|
||||
| models.Q(name__icontains=self.query)
|
||||
)
|
||||
return identities
|
||||
|
||||
|
||||
@method_decorator(admin_required, name="dispatch")
|
||||
class IdentityEdit(FormView):
|
||||
|
||||
template_name = "admin/identity_edit.html"
|
||||
extra_context = {
|
||||
"section": "identities",
|
||||
}
|
||||
|
||||
class form_class(forms.Form):
|
||||
notes = forms.CharField(widget=forms.Textarea, required=False)
|
||||
|
||||
def dispatch(self, request, id, *args, **kwargs):
|
||||
self.identity = get_object_or_404(Identity, id=id)
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if "fetch" in request.POST:
|
||||
self.identity.transition_perform(IdentityStates.outdated)
|
||||
self.identity = Identity.objects.get(pk=self.identity.pk)
|
||||
if "limit" in request.POST:
|
||||
self.identity.restriction = Identity.Restriction.limited
|
||||
self.identity.save()
|
||||
if "block" in request.POST:
|
||||
self.identity.restriction = Identity.Restriction.blocked
|
||||
self.identity.save()
|
||||
if "unlimit" in request.POST or "unblock" in request.POST:
|
||||
self.identity.restriction = Identity.Restriction.none
|
||||
self.identity.save()
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
def get_initial(self):
|
||||
return {"notes": self.identity.admin_notes}
|
||||
|
||||
def form_valid(self, form):
|
||||
self.identity.admin_notes = form.cleaned_data["notes"]
|
||||
self.identity.save()
|
||||
return redirect(".")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["identity"] = self.identity
|
||||
return context
|
@ -12,7 +12,7 @@ from users.models import User
|
||||
class UsersRoot(ListView):
|
||||
|
||||
template_name = "admin/users.html"
|
||||
paginate_by = 50
|
||||
paginate_by = 30
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.query = request.GET.get("query")
|
||||
|
Reference in New Issue
Block a user