diff --git a/README.md b/README.md index 1b3bcb2..17a9cad 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,14 @@ ![takahē](static/img/logo-128.png) -A *very experimental* Fediverse server for microblogging/"toots". Not fully functional yet - +An *experimental* Fediverse server for microblogging/"toots". Not fully functional yet - I'm still working on making all the basic bits work! For more background and information, -see [my blog posts about it](https://aeracode.org/category/takahe/). +see [jointakahe.org]](https://jointakahe.org/). -Indended features: - -* Can run on serverless hosting (no need for worker daemons) -* Multiple account domains possible per server -* Async evented core for fan-out/delivery -* Mastodon client API compatible (eventually) ## Deployment -### Requirements: - -- **Python** 3.11 -- **PostgreSQL** 14+ -- **Lots of patience** This is *very experimental* - -### Setup - -More deployment docs will come soon! Just know that you need to run the Takahē -Django app, and then either hit `/.stator/runner/` or run `./manage.py runstator` -at least every 30 seconds. +See [the documentation](https://takahe-server.readthedocs.io) ## Roadmap diff --git a/static/css/style.css b/static/css/style.css index 426308d..1584e68 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -617,6 +617,7 @@ form .button:hover { height: auto; object-fit: cover; margin: -65px -15px 0 -15px; + border-radius: 5px 0 0 0; display: block; } @@ -633,6 +634,7 @@ h1.identity .banner { display: block; width: calc(100% + 30px); margin: -65px -15px 20px -15px; + border-radius: 5px 0 0 0; } h1.identity .icon { @@ -644,10 +646,10 @@ h1.identity .icon { h1.identity small { display: block; - font-size: 80%; + font-size: 60%; font-weight: normal; color: var(--color-text-dull); - margin: -10px 0 0 0; + margin: -5px 0 0 0; } .bio { @@ -834,6 +836,11 @@ h1.identity small { header menu a.identity { width: 50px; padding: 10px 10px 0 0; + font-size: 0; + } + + header menu a.identity i { + font-size: 22px; } .right-column { diff --git a/static/img/icon-admin-512.png b/static/img/icon-admin-512.png new file mode 100644 index 0000000..1a0ada9 Binary files /dev/null and b/static/img/icon-admin-512.png differ diff --git a/static/img/icon-admin.svg b/static/img/icon-admin.svg new file mode 100644 index 0000000..d495b6f --- /dev/null +++ b/static/img/icon-admin.svg @@ -0,0 +1,66 @@ + + + + diff --git a/users/models/identity.py b/users/models/identity.py index 21912ac..53b6f80 100644 --- a/users/models/identity.py +++ b/users/models/identity.py @@ -162,6 +162,8 @@ class Identity(StatorModel): actor_uri, handle = async_to_sync(cls.fetch_webfinger)( f"{username}@{domain}" ) + if handle is None: + return None username, domain = handle.split("@") domain = Domain.get_remote_domain(domain) return cls.objects.create( diff --git a/users/views/settings.py b/users/views/settings/__init__.py similarity index 70% rename from users/views/settings.py rename to users/views/settings/__init__.py index 1403821..65be1c5 100644 --- a/users/views/settings.py +++ b/users/views/settings/__init__.py @@ -6,10 +6,10 @@ from django.core.files import File from django.shortcuts import redirect from django.utils.decorators import method_decorator from django.views.generic import FormView, RedirectView -from PIL import Image, ImageOps from core.models.config import Config, UploadedImage from users.decorators import identity_required +from users.views.settings.profile import ProfilePage # noqa @method_decorator(identity_required, name="dispatch") @@ -119,59 +119,6 @@ class InterfacePage(SettingsPage): layout = {"Posting": ["toot_mode"]} -@method_decorator(identity_required, name="dispatch") -class ProfilePage(FormView): - """ - Lets the identity's profile be edited - """ - - template_name = "settings/profile.html" - extra_context = {"section": "profile"} - - class form_class(forms.Form): - name = forms.CharField(max_length=500) - summary = forms.CharField( - widget=forms.Textarea, - required=False, - help_text="Describe you and your interests", - label="Bio", - ) - icon = forms.ImageField( - required=False, help_text="Shown next to all your posts and activities" - ) - image = forms.ImageField( - required=False, help_text="Shown at the top of your profile" - ) - - def get_initial(self): - return { - "name": self.request.identity.name, - "summary": self.request.identity.summary, - "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): - # Update identity name and summary - self.request.identity.name = form.cleaned_data["name"] - self.request.identity.summary = form.cleaned_data["summary"] - # Resize images - icon = form.cleaned_data.get("icon") - image = form.cleaned_data.get("image") - if isinstance(icon, File): - resized_image = ImageOps.fit(Image.open(icon), (400, 400)) - icon.open() - resized_image.save(icon) - self.request.identity.icon = icon - if isinstance(image, File): - resized_image = ImageOps.fit(Image.open(image), (1500, 500)) - image.open() - resized_image.save(image) - self.request.identity.image = image - self.request.identity.save() - return redirect(".") - - @method_decorator(identity_required, name="dispatch") class SecurityPage(FormView): """ diff --git a/users/views/settings/profile.py b/users/views/settings/profile.py new file mode 100644 index 0000000..3b16d69 --- /dev/null +++ b/users/views/settings/profile.py @@ -0,0 +1,63 @@ +import io + +from django import forms +from django.core.files import File +from django.shortcuts import redirect +from django.utils.decorators import method_decorator +from django.views.generic import FormView +from PIL import Image, ImageOps + +from users.decorators import identity_required + + +@method_decorator(identity_required, name="dispatch") +class ProfilePage(FormView): + """ + Lets the identity's profile be edited + """ + + template_name = "settings/profile.html" + extra_context = {"section": "profile"} + + class form_class(forms.Form): + name = forms.CharField(max_length=500) + summary = forms.CharField( + widget=forms.Textarea, + required=False, + help_text="Describe you and your interests", + label="Bio", + ) + icon = forms.ImageField( + required=False, help_text="Shown next to all your posts and activities" + ) + image = forms.ImageField( + required=False, help_text="Shown at the top of your profile" + ) + + def get_initial(self): + return { + "name": self.request.identity.name, + "summary": self.request.identity.summary, + "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): + # Update identity name and summary + self.request.identity.name = form.cleaned_data["name"] + self.request.identity.summary = form.cleaned_data["summary"] + # Resize images + icon = form.cleaned_data.get("icon") + image = form.cleaned_data.get("image") + if isinstance(icon, File): + resized_image = ImageOps.fit(Image.open(icon), (400, 400)) + new_icon_bytes = io.BytesIO() + resized_image.save(new_icon_bytes, format=icon.format) + self.request.identity.icon.save(icon.name, File(new_icon_bytes)) + if isinstance(image, File): + resized_image = ImageOps.fit(Image.open(image), (400, 400)) + new_image_bytes = io.BytesIO() + resized_image.save(new_image_bytes, format=image.format) + self.request.identity.image.save(image.name, File(new_image_bytes)) + self.request.identity.save() + return redirect(".")