From 81de10b70c85c5222b17d8c4358a8aa8812f2559 Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Fri, 18 Nov 2022 08:28:15 -0700 Subject: Migration reset, start of docs, env vars --- users/views/activitypub.py | 59 ++++++++++++++++++++++++++++++++++++-------- users/views/admin/domains.py | 52 ++++++++++++++++++++++++-------------- users/views/auth.py | 5 ++++ users/views/identity.py | 32 +++++++++++++++++++++--- users/views/settings.py | 4 +-- 5 files changed, 118 insertions(+), 34 deletions(-) (limited to 'users/views') diff --git a/users/views/activitypub.py b/users/views/activitypub.py index 4660d7a..2719f17 100644 --- a/users/views/activitypub.py +++ b/users/views/activitypub.py @@ -1,18 +1,22 @@ import json from asgiref.sync import async_to_sync +from django.conf import settings from django.http import Http404, HttpResponse, HttpResponseBadRequest, JsonResponse from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt from django.views.generic import View +from activities.models import Post from core.ld import canonicalise +from core.models import Config from core.signatures import ( HttpSignature, LDSignature, VerificationError, VerificationFormatError, ) +from takahe import __version__ from users.models import Identity, InboxMessage from users.shortcuts import by_handle_or_404 @@ -37,6 +41,51 @@ class HostMeta(View): ) +class NodeInfo(View): + """ + Returns the well-known nodeinfo response, pointing to the 2.0 one + """ + + def get(self, request): + host = request.META.get("HOST", settings.MAIN_DOMAIN) + return JsonResponse( + { + "links": [ + { + "rel": "http://nodeinfo.diaspora.software/ns/schema/2.0", + "href": f"https://{host}/nodeinfo/2.0/", + } + ] + } + ) + + +class NodeInfo2(View): + """ + Returns the nodeinfo 2.0 response + """ + + def get(self, request): + # Fetch some user stats + local_identities = Identity.objects.filter(local=True).count() + local_posts = Post.objects.filter(local=True).count() + return JsonResponse( + { + "version": "2.0", + "software": {"name": "takahe", "version": __version__}, + "protocols": ["activitypub"], + "services": {"outbound": [], "inbound": []}, + "usage": { + "users": {"total": local_identities}, + "localPosts": local_posts, + }, + "openRegistrations": Config.system.signup_allowed + and not Config.system.signup_invite_only, + "metadata": {}, + } + ) + + class Webfinger(View): """ Services webfinger requests @@ -70,16 +119,6 @@ class Webfinger(View): ) -class Actor(View): - """ - Returns the AP Actor object - """ - - def get(self, request, handle): - identity = by_handle_or_404(self.request, handle) - return JsonResponse(canonicalise(identity.to_ap(), include_security=True)) - - @method_decorator(csrf_exempt, name="dispatch") class Inbox(View): """ diff --git a/users/views/admin/domains.py b/users/views/admin/domains.py index e1a011b..c42137c 100644 --- a/users/views/admin/domains.py +++ b/users/views/admin/domains.py @@ -41,6 +41,11 @@ class DomainCreate(FormView): widget=forms.Select(choices=[(True, "Public"), (False, "Private")]), required=False, ) + default = forms.BooleanField( + help_text="If this is the default option for new identities", + widget=forms.Select(choices=[(True, "Yes"), (False, "No")]), + required=False, + ) domain_regex = re.compile( r"^((?!-))(xn--)?[a-z0-9][a-z0-9-_]{0,61}[a-z0-9]{0,1}\.(xn--)?([a-z0-9\-]{1,61}|[a-z0-9-]{1,30}\.[a-z]{2,})$" @@ -72,13 +77,22 @@ class DomainCreate(FormView): ) return self.cleaned_data["service_domain"] + def clean_default(self): + value = self.cleaned_data["default"] + if value and not self.cleaned_data.get("public"): + raise forms.ValidationError("A non-public domain cannot be the default") + return value + def form_valid(self, form): - Domain.objects.create( + domain = Domain.objects.create( domain=form.cleaned_data["domain"], service_domain=form.cleaned_data["service_domain"] or None, public=form.cleaned_data["public"], + default=form.cleaned_data["default"], local=True, ) + if domain.default: + Domain.objects.exclude(pk=domain.pk).update(default=False) return redirect(Domain.urls.root) @@ -88,21 +102,17 @@ class DomainEdit(FormView): template_name = "admin/domain_edit.html" extra_context = {"section": "domains"} - class form_class(forms.Form): - domain = forms.CharField( - help_text="The domain displayed as part of a user's identity.\nCannot be changed after the domain has been created.", - disabled=True, - ) - service_domain = forms.CharField( - help_text="Optional - a domain that serves Takahē if it is not running on the main domain.\nCannot be changed after the domain has been created.", - disabled=True, - required=False, - ) - public = forms.BooleanField( - help_text="If any user on this server can create identities here", - widget=forms.Select(choices=[(True, "Public"), (False, "Private")]), - required=False, - ) + class form_class(DomainCreate.form_class): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["domain"].disabled = True + self.fields["service_domain"].disabled = True + + def clean_domain(self): + return self.cleaned_data["domain"] + + def clean_service_domain(self): + return self.cleaned_data["service_domain"] def dispatch(self, request, domain): self.domain = get_object_or_404( @@ -110,14 +120,17 @@ class DomainEdit(FormView): ) return super().dispatch(request) - def get_context_data(self): - context = super().get_context_data() + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) context["domain"] = self.domain return context def form_valid(self, form): self.domain.public = form.cleaned_data["public"] + self.domain.default = form.cleaned_data["default"] self.domain.save() + if self.domain.default: + Domain.objects.exclude(pk=self.domain.pk).update(default=False) return redirect(Domain.urls.root) def get_initial(self): @@ -125,6 +138,7 @@ class DomainEdit(FormView): "domain": self.domain.domain, "service_domain": self.domain.service_domain, "public": self.domain.public, + "default": self.domain.default, } @@ -150,4 +164,4 @@ class DomainDelete(TemplateView): if self.domain.identities.exists(): raise ValueError("Tried to delete domain with identities!") self.domain.delete() - return redirect("/settings/system/domains/") + return redirect("admin_domains") diff --git a/users/views/auth.py b/users/views/auth.py index a04b1b1..2257ea5 100644 --- a/users/views/auth.py +++ b/users/views/auth.py @@ -1,4 +1,5 @@ from django import forms +from django.conf import settings from django.contrib.auth.password_validation import validate_password from django.contrib.auth.views import LoginView, LogoutView from django.shortcuts import get_object_or_404, render @@ -50,6 +51,10 @@ class Signup(FormView): def form_valid(self, form): 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) if "invite_code" in form.cleaned_data: Invite.objects.filter(token=form.cleaned_data["invite_code"]).delete() diff --git a/users/views/identity.py b/users/views/identity.py index ae8e5b0..5524c4c 100644 --- a/users/views/identity.py +++ b/users/views/identity.py @@ -2,11 +2,12 @@ import string from django import forms from django.contrib.auth.decorators import login_required -from django.http import Http404 +from django.http import Http404, JsonResponse from django.shortcuts import redirect from django.utils.decorators import method_decorator from django.views.generic import FormView, TemplateView, View +from core.ld import canonicalise from core.models import Config from users.decorators import identity_required from users.models import Domain, Follow, FollowStates, Identity, IdentityStates @@ -14,16 +15,41 @@ from users.shortcuts import by_handle_or_404 class ViewIdentity(TemplateView): + """ + Shows identity profile pages, and also acts as the Actor endpoint when + approached with the right Accept header. + """ template_name = "identity/view.html" - def get_context_data(self, handle): + def get(self, request, handle): + # Make sure we understand this handle identity = by_handle_or_404( self.request, handle, local=False, fetch=True, ) + # If they're coming in looking for JSON, they want the actor + accept = request.META.get("HTTP_ACCEPT", "text/html").lower() + if ( + "application/json" in accept + or "application/ld" in accept + or "application/activity" in accept + ): + # Return actor info + return self.serve_actor(identity) + else: + # Show normal page + return super().get(request, identity=identity) + + def serve_actor(self, identity): + # If this not a local actor, redirect to their canonical URI + if not identity.local: + return redirect(identity.actor_uri) + return JsonResponse(canonicalise(identity.to_ap(), include_security=True)) + + def get_context_data(self, identity): posts = identity.posts.all()[:100] if identity.data_age > Config.system.identity_max_age: identity.transition_perform(IdentityStates.outdated) @@ -150,7 +176,7 @@ class CreateIdentity(FormView): 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}/actor/", + actor_uri=f"https://{domain_instance.uri_domain}/@{username}@{domain}/", username=username.lower(), domain_id=domain, name=form.cleaned_data["name"], diff --git a/users/views/settings.py b/users/views/settings.py index fd138c2..1403821 100644 --- a/users/views/settings.py +++ b/users/views/settings.py @@ -147,8 +147,8 @@ class ProfilePage(FormView): return { "name": self.request.identity.name, "summary": self.request.identity.summary, - "icon": self.request.identity.icon.url, - "image": self.request.identity.image.url, + "icon": self.request.identity.icon and self.request.identity.icon.url, + "image": self.request.identity.image and self.request.identity.image.url, } def form_valid(self, form): -- cgit v1.2.3