Fixed #421: Allow profile editing via API

This commit is contained in:
Andrew Godwin 2023-01-15 16:15:57 -07:00
parent e39355ceb5
commit de9261251e
6 changed files with 108 additions and 17 deletions

View file

@ -51,6 +51,7 @@ class Account(Schema):
statuses_count: int
followers_count: int
following_count: int
source: dict | None
class MediaAttachment(Schema):

View file

@ -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])

View file

@ -2,7 +2,7 @@
<div class="actions" role="menubar">
{% if request.identity == identity %}
<a href="{% url "settings_profile" %}" class="button" title="Edit Profile">
<i class="fa-solid fa-user-edit"></i>
<i class="fa-solid fa-user-edit"></i> Edit
</a>
{% elif not inbound_block %}
{% if inbound_follow or outbound_mute %}

View file

@ -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 ###

View file

@ -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)),
)

View file

@ -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