Fixed #421: Allow profile editing via API
This commit is contained in:
parent
e39355ceb5
commit
de9261251e
|
@ -51,6 +51,7 @@ class Account(Schema):
|
|||
statuses_count: int
|
||||
followers_count: int
|
||||
following_count: int
|
||||
source: dict | None
|
||||
|
||||
|
||||
class MediaAttachment(Schema):
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 ###
|
||||
|
||||
|
|
|
@ -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)),
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue