summaryrefslogtreecommitdiffstats
path: root/users/views
diff options
context:
space:
mode:
Diffstat (limited to 'users/views')
-rw-r--r--users/views/activitypub.py59
-rw-r--r--users/views/admin/domains.py52
-rw-r--r--users/views/auth.py5
-rw-r--r--users/views/identity.py32
-rw-r--r--users/views/settings.py4
5 files changed, 118 insertions, 34 deletions
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):