Add admin notification for new identities
This commit is contained in:
parent
34b4a6cc10
commit
9dded19172
|
@ -241,6 +241,13 @@ class FanOutStates(StateGraph):
|
|||
case (FanOut.Types.identity_deleted, True):
|
||||
pass
|
||||
|
||||
# Created identities make a timeline event
|
||||
case (FanOut.Types.identity_created, True):
|
||||
await sync_to_async(TimelineEvent.add_identity_created)(
|
||||
identity=fan_out.identity,
|
||||
new_identity=fan_out.subject_identity,
|
||||
)
|
||||
|
||||
case _:
|
||||
raise ValueError(
|
||||
f"Cannot fan out with type {fan_out.type} local={fan_out.identity.local}"
|
||||
|
@ -262,6 +269,7 @@ class FanOut(StatorModel):
|
|||
undo_interaction = "undo_interaction"
|
||||
identity_edited = "identity_edited"
|
||||
identity_deleted = "identity_deleted"
|
||||
identity_created = "identity_created"
|
||||
|
||||
state = StateField(FanOutStates)
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ class TimelineEvent(models.Model):
|
|||
followed = "followed"
|
||||
boosted = "boosted" # Someone boosting one of our posts
|
||||
announcement = "announcement" # Server announcement
|
||||
identity_created = "identity_created" # New identity created
|
||||
|
||||
# The user this event is for
|
||||
identity = models.ForeignKey(
|
||||
|
@ -103,6 +104,17 @@ class TimelineEvent(models.Model):
|
|||
defaults={"published": post.published or post.created},
|
||||
)[0]
|
||||
|
||||
@classmethod
|
||||
def add_identity_created(cls, identity, new_identity):
|
||||
"""
|
||||
Adds a new identity item
|
||||
"""
|
||||
return cls.objects.get_or_create(
|
||||
identity=identity,
|
||||
type=cls.Types.identity_created,
|
||||
subject_identity=new_identity,
|
||||
)[0]
|
||||
|
||||
@classmethod
|
||||
def add_post_interaction(cls, identity, interaction):
|
||||
"""
|
||||
|
@ -179,6 +191,8 @@ class TimelineEvent(models.Model):
|
|||
)
|
||||
elif self.type == self.Types.followed:
|
||||
result["type"] = "follow"
|
||||
elif self.type == self.Types.identity_created:
|
||||
result["type"] = "admin.sign_up"
|
||||
else:
|
||||
raise ValueError(f"Cannot convert {self.type} to notification JSON")
|
||||
return result
|
||||
|
|
|
@ -129,6 +129,7 @@ class Notifications(ListView):
|
|||
"boosted": TimelineEvent.Types.boosted,
|
||||
"mentioned": TimelineEvent.Types.mentioned,
|
||||
"liked": TimelineEvent.Types.liked,
|
||||
"identity_created": TimelineEvent.Types.identity_created,
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
|
|
|
@ -25,6 +25,7 @@ def notifications(
|
|||
"reblog": TimelineEvent.Types.boosted,
|
||||
"mention": TimelineEvent.Types.mentioned,
|
||||
"follow": TimelineEvent.Types.followed,
|
||||
"admin.sign_up": TimelineEvent.Types.identity_created,
|
||||
}
|
||||
requested_types = set(request.GET.getlist("types[]"))
|
||||
excluded_types = set(request.GET.getlist("exclude_types[]"))
|
||||
|
|
|
@ -214,6 +214,7 @@ class Config(models.Model):
|
|||
signup_allowed: bool = True
|
||||
signup_text: str = ""
|
||||
signup_max_users: int = 0
|
||||
signup_email_admins: bool = True
|
||||
content_warning_text: str = "Content Warning"
|
||||
|
||||
post_length: int = 500
|
||||
|
|
|
@ -34,6 +34,13 @@
|
|||
{% if not event.collapsed %}
|
||||
{% include "activities/_post.html" with post=event.subject_post event=event %}
|
||||
{% endif %}
|
||||
{% elif event.type == "identity_created" %}
|
||||
<div class="follow-banner">
|
||||
<a href="{{ event.subject_identity.urls.view }}">
|
||||
{{ event.subject_identity.html_name_or_handle }}
|
||||
</a> was just created
|
||||
</div>
|
||||
{% include "activities/_identity.html" with identity=event.subject_identity created=event.created %}
|
||||
{% else %}
|
||||
Unknown event type {{ event.type }}
|
||||
{% endif %}
|
||||
|
|
|
@ -28,6 +28,13 @@
|
|||
{% else %}
|
||||
<a href=".?mentioned=true"><i class="fa-solid fa-xmark"></i> Mentions</a>
|
||||
{% endif %}
|
||||
{% if request.user.admin %}
|
||||
{% if notification_options.identity_created %}
|
||||
<a href=".?identity_created=false" class="selected"><i class="fa-solid fa-check"></i> New Identities</a>
|
||||
{% else %}
|
||||
<a href=".?identity_created=true"><i class="fa-solid fa-xmark"></i> New Identities</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% for event in events %}
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
from .announcement import AnnouncementService # noqa
|
||||
from .identity import IdentityService # noqa
|
||||
from .user import UserService # noqa
|
||||
|
|
|
@ -1,8 +1,17 @@
|
|||
from django.db import models
|
||||
from django.template.defaultfilters import linebreaks_filter
|
||||
|
||||
from activities.models import FanOut
|
||||
from core.html import strip_html
|
||||
from users.models import Block, BlockStates, Follow, FollowStates, Identity
|
||||
from users.models import (
|
||||
Block,
|
||||
BlockStates,
|
||||
Domain,
|
||||
Follow,
|
||||
FollowStates,
|
||||
Identity,
|
||||
User,
|
||||
)
|
||||
|
||||
|
||||
class IdentityService:
|
||||
|
@ -13,6 +22,38 @@ class IdentityService:
|
|||
def __init__(self, identity: Identity):
|
||||
self.identity = identity
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
user: User,
|
||||
username: str,
|
||||
domain: Domain,
|
||||
name: str,
|
||||
discoverable: bool = True,
|
||||
) -> Identity:
|
||||
identity = Identity.objects.create(
|
||||
actor_uri=f"https://{domain.uri_domain}/@{username}@{domain.domain}/",
|
||||
username=username,
|
||||
domain=domain,
|
||||
name=name,
|
||||
local=True,
|
||||
discoverable=discoverable,
|
||||
)
|
||||
identity.users.add(user)
|
||||
identity.generate_keypair()
|
||||
# Send fanouts to all admin identities
|
||||
for admin_identity in cls.admin_identities():
|
||||
FanOut.objects.create(
|
||||
type=FanOut.Types.identity_created,
|
||||
identity=admin_identity,
|
||||
subject_identity=identity,
|
||||
)
|
||||
return identity
|
||||
|
||||
@classmethod
|
||||
def admin_identities(cls) -> models.QuerySet[Identity]:
|
||||
return Identity.objects.filter(users__admin=True).distinct()
|
||||
|
||||
def following(self) -> models.QuerySet[Identity]:
|
||||
return (
|
||||
Identity.objects.filter(inbound_follows__source=self.identity)
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
from users.models import PasswordReset, User
|
||||
|
||||
|
||||
class UserService:
|
||||
"""
|
||||
High-level user handling methods
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def admins(cls) -> models.QuerySet[User]:
|
||||
return User.objects.filter(admin=True)
|
||||
|
||||
@classmethod
|
||||
def moderators(cls) -> models.QuerySet[User]:
|
||||
return User.objects.filter(models.Q(moderator=True) | models.Q(admin=True))
|
||||
|
||||
@classmethod
|
||||
def create(cls, email: str) -> User:
|
||||
"""
|
||||
Creates a new user
|
||||
"""
|
||||
# Make the new user
|
||||
user = User.objects.create(email=email)
|
||||
# Auto-promote the user to admin if that setting is set
|
||||
if settings.AUTO_ADMIN_EMAIL and user.email == settings.AUTO_ADMIN_EMAIL:
|
||||
user.admin = True
|
||||
user.save()
|
||||
# Send them a password reset email
|
||||
PasswordReset.create_for_user(user)
|
||||
return user
|
|
@ -80,6 +80,10 @@ class BasicSettings(AdminSettingsPage):
|
|||
"title": "Maximum User Limit",
|
||||
"help_text": "Signups will be auto-disabled if your server grows to this many users.\nUse 0 for unlimited.",
|
||||
},
|
||||
"signup_email_admins": {
|
||||
"title": "Email admins on signup",
|
||||
"help_text": "Send an email to all admins whenever a new user signs up",
|
||||
},
|
||||
"restricted_usernames": {
|
||||
"title": "Restricted Usernames",
|
||||
"help_text": "Usernames that only admins can register for identities. One per line.",
|
||||
|
@ -120,6 +124,7 @@ class BasicSettings(AdminSettingsPage):
|
|||
"Signups": [
|
||||
"signup_allowed",
|
||||
"signup_max_users",
|
||||
# "signup_email_admins",
|
||||
"signup_text",
|
||||
],
|
||||
"Posts": [
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import markdown_it
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.forms import AuthenticationForm
|
||||
from django.contrib.auth.password_validation import validate_password
|
||||
from django.contrib.auth.views import LoginView, LogoutView
|
||||
|
@ -12,6 +11,7 @@ from django.views.generic import FormView
|
|||
|
||||
from core.models import Config
|
||||
from users.models import Invite, PasswordReset, User
|
||||
from users.services import UserService
|
||||
|
||||
|
||||
class Login(LoginView):
|
||||
|
@ -99,13 +99,8 @@ class Signup(FormView):
|
|||
# Don't allow anything if there's no invite and no signup allowed
|
||||
if (not Config.system.signup_allowed or self.at_max_users) and not self.invite:
|
||||
return self.render_to_response(self.get_context_data())
|
||||
# Make the new user
|
||||
user = User.objects.create(email=form.cleaned_data["email"])
|
||||
# Auto-promote the user to admin if that setting is set
|
||||
if settings.AUTO_ADMIN_EMAIL and user.email == settings.AUTO_ADMIN_EMAIL:
|
||||
user.admin = True
|
||||
user.save()
|
||||
PasswordReset.create_for_user(user)
|
||||
# Make the user
|
||||
user = UserService.create(email=form.cleaned_data["email"])
|
||||
# Drop invite uses down if it has them
|
||||
if self.invite and self.invite.uses is not None:
|
||||
self.invite.uses -= 1
|
||||
|
|
|
@ -381,15 +381,12 @@ class CreateIdentity(FormView):
|
|||
username = form.cleaned_data["username"]
|
||||
domain = form.cleaned_data["domain"]
|
||||
domain_instance = Domain.get_domain(domain)
|
||||
new_identity = Identity.objects.create(
|
||||
actor_uri=f"https://{domain_instance.uri_domain}/@{username}@{domain}/",
|
||||
identity = IdentityService.create(
|
||||
user=self.request.user,
|
||||
username=username,
|
||||
domain_id=domain,
|
||||
domain=domain_instance,
|
||||
name=form.cleaned_data["name"],
|
||||
local=True,
|
||||
discoverable=form.cleaned_data["discoverable"],
|
||||
)
|
||||
new_identity.users.add(self.request.user)
|
||||
new_identity.generate_keypair()
|
||||
self.request.session["identity_id"] = new_identity.id
|
||||
return redirect(new_identity.urls.view)
|
||||
self.request.session["identity_id"] = identity.id
|
||||
return redirect(identity.urls.view)
|
||||
|
|
Loading…
Reference in New Issue