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