summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md22
-rw-r--r--static/css/style.css11
-rw-r--r--static/img/icon-admin-512.pngbin0 -> 48813 bytes
-rw-r--r--static/img/icon-admin.svg66
-rw-r--r--users/models/identity.py2
-rw-r--r--users/views/settings/__init__.py (renamed from users/views/settings.py)55
-rw-r--r--users/views/settings/profile.py63
7 files changed, 144 insertions, 75 deletions
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
--- /dev/null
+++ b/static/img/icon-admin-512.png
Binary files 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="1024"
+ height="1024"
+ viewBox="0 0 270.93333 270.93333"
+ version="1.1"
+ id="svg5"
+ xml:space="preserve"
+ inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
+ sodipodi:docname="icon-admin.svg"
+ inkscape:export-filename="icon-admin-512.png"
+ inkscape:export-xdpi="12.000001"
+ inkscape:export-ydpi="12.000001"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
+ id="namedview7"
+ pagecolor="#ffffff"
+ bordercolor="#000000"
+ borderopacity="0.25"
+ inkscape:showpageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#d1d1d1"
+ inkscape:document-units="mm"
+ showgrid="false"
+ inkscape:zoom="0.5946522"
+ inkscape:cx="761.78983"
+ inkscape:cy="461.61437"
+ inkscape:current-layer="layer1"><inkscape:grid
+ type="xygrid"
+ id="grid111" /></sodipodi:namedview><defs
+ id="defs2"><linearGradient
+ inkscape:collect="always"
+ id="linearGradient717"><stop
+ style="stop-color:#dc0000;stop-opacity:1;"
+ offset="0"
+ id="stop713" /><stop
+ style="stop-color:#b50000;stop-opacity:1"
+ offset="1"
+ id="stop715" /></linearGradient><linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient717"
+ id="linearGradient719"
+ x1="228.25317"
+ y1="14.682952"
+ x2="140.6004"
+ y2="261.17859"
+ gradientUnits="userSpaceOnUse" /></defs><g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"><rect
+ style="fill:url(#linearGradient719);fill-opacity:1;stroke-width:0.264583"
+ id="rect115"
+ width="270.93332"
+ height="270.93332"
+ x="0"
+ y="0"
+ rx="33.866665" /><path
+ id="path18329"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke-width:0.264583"
+ d="M 81.445158 44.391606 C 71.497762 44.34326 59.998982 45.079969 48.053398 47.437415 C 15.973572 53.768324 -5.0132687e-15 69.410234 0 69.410234 L 0 270.93333 L 126.36231 270.93333 C 126.36231 270.93333 127.25234 270.93314 122.02201 253.61449 C 116.79165 236.29585 123.37369 206.01207 150.06991 203.11525 C 152.12765 202.89196 153.29599 201.80052 153.81697 200.1361 C 163.3758 199.05018 175.31632 198.19731 189.82666 198.01634 C 221.49006 197.62143 257.75843 201.03217 257.75843 201.03217 C 257.75843 201.03217 245.16914 168.19766 229.14105 143.28169 C 212.31497 117.12523 195.16587 101.64392 195.16587 101.64392 C 179.27533 82.720553 145.61897 53.85519 113.77238 47.437415 C 113.77238 47.437415 100.43564 44.483902 81.445158 44.391606 z M 94.104333 114.79403 A 17.130112 17.130112 0 0 1 111.23455 131.92425 A 17.130112 17.130112 0 0 1 94.104333 149.05447 A 17.130112 17.130112 0 0 1 76.974113 131.92425 A 17.130112 17.130112 0 0 1 94.104333 114.79403 z " /></g></svg>
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
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")
@@ -120,59 +120,6 @@ class InterfacePage(SettingsPage):
@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):
"""
Lets the identity's profile be edited
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(".")