diff --git a/api/schemas.py b/api/schemas.py
index 8163799..60035fe 100644
--- a/api/schemas.py
+++ b/api/schemas.py
@@ -51,6 +51,7 @@ class Account(Schema):
statuses_count: int
followers_count: int
following_count: int
+ source: dict | None
class MediaAttachment(Schema):
diff --git a/api/views/accounts.py b/api/views/accounts.py
index a49bed2..b401ddd 100644
--- a/api/views/accounts.py
+++ b/api/views/accounts.py
@@ -1,12 +1,15 @@
-from django.http import HttpRequest, HttpResponse
+from django.http import HttpRequest, HttpResponse, QueryDict
+from django.http.multipartparser import MultiPartParser
from django.shortcuts import get_object_or_404
from ninja import Field, Schema
+from activities.models import Post
from activities.services import SearchService
from api import schemas
from api.decorators import identity_required
from api.pagination import MastodonPaginator
from api.views.base import api_router
+from core.models import Config
from users.models import Identity
from users.services import IdentityService
from users.shortcuts import by_handle_or_404
@@ -15,7 +18,62 @@ from users.shortcuts import by_handle_or_404
@api_router.get("/v1/accounts/verify_credentials", response=schemas.Account)
@identity_required
def verify_credentials(request):
- return request.identity.to_mastodon_json()
+ return request.identity.to_mastodon_json(source=True)
+
+
+@api_router.patch("/v1/accounts/update_credentials", response=schemas.Account)
+@identity_required
+def update_credentials(
+ request,
+):
+ # Django won't load POST and FILES for patch methods, so we do it.
+ if request.content_type == "multipart/form-data":
+ POST, FILES = MultiPartParser(
+ request.META, request, request.upload_handlers, request.encoding
+ ).parse()
+ elif request.content_type == "application/x-www-form-urlencoded":
+ POST = QueryDict(request.body, encoding=request._encoding)
+ FILES = {}
+ else:
+ return HttpResponse(status=400)
+ identity = request.identity
+ service = IdentityService(identity)
+ if "display_name" in POST:
+ identity.name = POST["display_name"]
+ if "note" in POST:
+ service.set_summary(POST["note"])
+ if "discoverable" in POST:
+ identity.discoverable = POST["discoverable"] == "checked"
+ if "source[privacy]" in POST:
+ privacy_map = {
+ "public": Post.Visibilities.public,
+ "unlisted": Post.Visibilities.unlisted,
+ "private": Post.Visibilities.followers,
+ "direct": Post.Visibilities.mentioned,
+ }
+ Config.set_identity(
+ identity,
+ "default_post_visibility",
+ privacy_map[POST["source[privacy]"]],
+ )
+ if "fields_attributes[0][name]" in POST:
+ identity.metadata = []
+ for i in range(4):
+ name_name = f"fields_attributes[{i}][name]"
+ value_name = f"fields_attributes[{i}][value]"
+ if name_name and value_name in POST:
+ # Empty value means delete this item
+ if not POST[value_name]:
+ break
+ identity.metadata.append(
+ {"name": POST[name_name], "value": POST[value_name]}
+ )
+ if "avatar" in FILES:
+ service.set_icon(FILES["avatar"])
+ if "header" in FILES:
+ service.set_image(FILES["header"])
+ identity.save()
+ return identity.to_mastodon_json(source=True)
@api_router.get("/v1/accounts/relationships", response=list[schemas.Relationship])
diff --git a/templates/identity/_view_menu.html b/templates/identity/_view_menu.html
index a88ff7e..a43a7bc 100644
--- a/templates/identity/_view_menu.html
+++ b/templates/identity/_view_menu.html
@@ -2,7 +2,7 @@
{% if request.identity == identity %}
-
+ Edit
{% elif not inbound_block %}
{% if inbound_follow or outbound_mute %}
diff --git a/users/models/identity.py b/users/models/identity.py
index d38dbaf..4e41949 100644
--- a/users/models/identity.py
+++ b/users/models/identity.py
@@ -13,7 +13,7 @@ from django.utils.functional import lazy
from lxml import etree
from core.exceptions import ActorMismatchError
-from core.html import ContentRenderer, strip_html
+from core.html import ContentRenderer, html_to_plaintext, strip_html
from core.ld import (
canonicalise,
format_ld_date,
@@ -830,8 +830,8 @@ class Identity(StatorModel):
"acct": self.handle or "",
}
- def to_mastodon_json(self, include_counts=True):
- from activities.models import Emoji
+ def to_mastodon_json(self, source=False, include_counts=True):
+ from activities.models import Emoji, Post
header_image = self.local_image_url()
missing = StaticAbsoluteUrl("img/missing.png").absolute
@@ -843,7 +843,7 @@ class Identity(StatorModel):
f"{self.name} {self.summary} {metadata_value_text}", self.domain
)
renderer = ContentRenderer(local=False)
- return {
+ result = {
"id": self.pk,
"username": self.username or "",
"acct": self.handle,
@@ -881,6 +881,25 @@ class Identity(StatorModel):
"followers_count": self.inbound_follows.count() if include_counts else 0,
"following_count": self.outbound_follows.count() if include_counts else 0,
}
+ if source:
+ privacy_map = {
+ Post.Visibilities.public: "public",
+ Post.Visibilities.unlisted: "unlisted",
+ Post.Visibilities.local_only: "unlisted",
+ Post.Visibilities.followers: "private",
+ Post.Visibilities.mentioned: "direct",
+ }
+ result["source"] = {
+ "note": html_to_plaintext(self.summary),
+ "fields": result["fields"],
+ "privacy": privacy_map[
+ Config.load_identity(self).default_post_visibility
+ ],
+ "sensitive": False,
+ "language": "unk",
+ "follow_requests_count": 0,
+ }
+ return result
### Cryptography ###
diff --git a/users/services/identity.py b/users/services/identity.py
index a6e44b1..3016165 100644
--- a/users/services/identity.py
+++ b/users/services/identity.py
@@ -2,6 +2,7 @@ from django.db import models
from django.template.defaultfilters import linebreaks_filter
from activities.models import FanOut
+from core.files import resize_image
from core.html import strip_html
from users.models import (
Block,
@@ -185,3 +186,21 @@ class IdentityService:
else:
self.identity.summary = None
self.identity.save()
+
+ def set_icon(self, file):
+ """
+ Sets the user's avatar image
+ """
+ self.identity.icon.save(
+ file.name,
+ resize_image(file, size=(400, 400)),
+ )
+
+ def set_image(self, file):
+ """
+ Sets the user's header image
+ """
+ self.identity.image.save(
+ file.name,
+ resize_image(file, size=(1500, 500)),
+ )
diff --git a/users/views/settings/profile.py b/users/views/settings/profile.py
index 9e4739a..7a2e957 100644
--- a/users/views/settings/profile.py
+++ b/users/views/settings/profile.py
@@ -4,7 +4,6 @@ from django.shortcuts import redirect
from django.utils.decorators import method_decorator
from django.views.generic import FormView
-from core.files import resize_image
from core.html import html_to_plaintext
from core.models.config import Config
from users.decorators import identity_required
@@ -77,22 +76,17 @@ class ProfilePage(FormView):
def form_valid(self, form):
# Update basic info
identity = self.request.identity
+ service = IdentityService(identity)
identity.name = form.cleaned_data["name"]
identity.discoverable = form.cleaned_data["discoverable"]
- IdentityService(identity).set_summary(form.cleaned_data["summary"])
+ service.set_summary(form.cleaned_data["summary"])
# Resize images
icon = form.cleaned_data.get("icon")
image = form.cleaned_data.get("image")
if isinstance(icon, File):
- identity.icon.save(
- icon.name,
- resize_image(icon, size=(400, 400)),
- )
+ service.set_icon(icon)
if isinstance(image, File):
- identity.image.save(
- image.name,
- resize_image(image, size=(1500, 500)),
- )
+ service.set_image(image)
identity.metadata = form.cleaned_data.get("metadata")
# Clear images if specified