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
|
statuses_count: int
|
||||||
followers_count: int
|
followers_count: int
|
||||||
following_count: int
|
following_count: int
|
||||||
|
source: dict | None
|
||||||
|
|
||||||
|
|
||||||
class MediaAttachment(Schema):
|
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 django.shortcuts import get_object_or_404
|
||||||
from ninja import Field, Schema
|
from ninja import Field, Schema
|
||||||
|
|
||||||
|
from activities.models import Post
|
||||||
from activities.services import SearchService
|
from activities.services import SearchService
|
||||||
from api import schemas
|
from api import schemas
|
||||||
from api.decorators import identity_required
|
from api.decorators import identity_required
|
||||||
from api.pagination import MastodonPaginator
|
from api.pagination import MastodonPaginator
|
||||||
from api.views.base import api_router
|
from api.views.base import api_router
|
||||||
|
from core.models import Config
|
||||||
from users.models import Identity
|
from users.models import Identity
|
||||||
from users.services import IdentityService
|
from users.services import IdentityService
|
||||||
from users.shortcuts import by_handle_or_404
|
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)
|
@api_router.get("/v1/accounts/verify_credentials", response=schemas.Account)
|
||||||
@identity_required
|
@identity_required
|
||||||
def verify_credentials(request):
|
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])
|
@api_router.get("/v1/accounts/relationships", response=list[schemas.Relationship])
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="actions" role="menubar">
|
<div class="actions" role="menubar">
|
||||||
{% if request.identity == identity %}
|
{% if request.identity == identity %}
|
||||||
<a href="{% url "settings_profile" %}" class="button" title="Edit Profile">
|
<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>
|
</a>
|
||||||
{% elif not inbound_block %}
|
{% elif not inbound_block %}
|
||||||
{% if inbound_follow or outbound_mute %}
|
{% if inbound_follow or outbound_mute %}
|
||||||
|
|
|
@ -13,7 +13,7 @@ from django.utils.functional import lazy
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
from core.exceptions import ActorMismatchError
|
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 (
|
from core.ld import (
|
||||||
canonicalise,
|
canonicalise,
|
||||||
format_ld_date,
|
format_ld_date,
|
||||||
|
@ -830,8 +830,8 @@ class Identity(StatorModel):
|
||||||
"acct": self.handle or "",
|
"acct": self.handle or "",
|
||||||
}
|
}
|
||||||
|
|
||||||
def to_mastodon_json(self, include_counts=True):
|
def to_mastodon_json(self, source=False, include_counts=True):
|
||||||
from activities.models import Emoji
|
from activities.models import Emoji, Post
|
||||||
|
|
||||||
header_image = self.local_image_url()
|
header_image = self.local_image_url()
|
||||||
missing = StaticAbsoluteUrl("img/missing.png").absolute
|
missing = StaticAbsoluteUrl("img/missing.png").absolute
|
||||||
|
@ -843,7 +843,7 @@ class Identity(StatorModel):
|
||||||
f"{self.name} {self.summary} {metadata_value_text}", self.domain
|
f"{self.name} {self.summary} {metadata_value_text}", self.domain
|
||||||
)
|
)
|
||||||
renderer = ContentRenderer(local=False)
|
renderer = ContentRenderer(local=False)
|
||||||
return {
|
result = {
|
||||||
"id": self.pk,
|
"id": self.pk,
|
||||||
"username": self.username or "",
|
"username": self.username or "",
|
||||||
"acct": self.handle,
|
"acct": self.handle,
|
||||||
|
@ -881,6 +881,25 @@ class Identity(StatorModel):
|
||||||
"followers_count": self.inbound_follows.count() if include_counts else 0,
|
"followers_count": self.inbound_follows.count() if include_counts else 0,
|
||||||
"following_count": self.outbound_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 ###
|
### Cryptography ###
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ from django.db import models
|
||||||
from django.template.defaultfilters import linebreaks_filter
|
from django.template.defaultfilters import linebreaks_filter
|
||||||
|
|
||||||
from activities.models import FanOut
|
from activities.models import FanOut
|
||||||
|
from core.files import resize_image
|
||||||
from core.html import strip_html
|
from core.html import strip_html
|
||||||
from users.models import (
|
from users.models import (
|
||||||
Block,
|
Block,
|
||||||
|
@ -185,3 +186,21 @@ class IdentityService:
|
||||||
else:
|
else:
|
||||||
self.identity.summary = None
|
self.identity.summary = None
|
||||||
self.identity.save()
|
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.utils.decorators import method_decorator
|
||||||
from django.views.generic import FormView
|
from django.views.generic import FormView
|
||||||
|
|
||||||
from core.files import resize_image
|
|
||||||
from core.html import html_to_plaintext
|
from core.html import html_to_plaintext
|
||||||
from core.models.config import Config
|
from core.models.config import Config
|
||||||
from users.decorators import identity_required
|
from users.decorators import identity_required
|
||||||
|
@ -77,22 +76,17 @@ class ProfilePage(FormView):
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
# Update basic info
|
# Update basic info
|
||||||
identity = self.request.identity
|
identity = self.request.identity
|
||||||
|
service = IdentityService(identity)
|
||||||
identity.name = form.cleaned_data["name"]
|
identity.name = form.cleaned_data["name"]
|
||||||
identity.discoverable = form.cleaned_data["discoverable"]
|
identity.discoverable = form.cleaned_data["discoverable"]
|
||||||
IdentityService(identity).set_summary(form.cleaned_data["summary"])
|
service.set_summary(form.cleaned_data["summary"])
|
||||||
# Resize images
|
# Resize images
|
||||||
icon = form.cleaned_data.get("icon")
|
icon = form.cleaned_data.get("icon")
|
||||||
image = form.cleaned_data.get("image")
|
image = form.cleaned_data.get("image")
|
||||||
if isinstance(icon, File):
|
if isinstance(icon, File):
|
||||||
identity.icon.save(
|
service.set_icon(icon)
|
||||||
icon.name,
|
|
||||||
resize_image(icon, size=(400, 400)),
|
|
||||||
)
|
|
||||||
if isinstance(image, File):
|
if isinstance(image, File):
|
||||||
identity.image.save(
|
service.set_image(image)
|
||||||
image.name,
|
|
||||||
resize_image(image, size=(1500, 500)),
|
|
||||||
)
|
|
||||||
identity.metadata = form.cleaned_data.get("metadata")
|
identity.metadata = form.cleaned_data.get("metadata")
|
||||||
|
|
||||||
# Clear images if specified
|
# Clear images if specified
|
||||||
|
|
Loading…
Reference in New Issue