summaryrefslogtreecommitdiffstats
path: root/api
diff options
context:
space:
mode:
Diffstat (limited to 'api')
-rw-r--r--api/schemas.py5
-rw-r--r--api/views/__init__.py4
-rw-r--r--api/views/accounts.py12
-rw-r--r--api/views/filters.py9
-rw-r--r--api/views/media.py76
-rw-r--r--api/views/notifications.py16
-rw-r--r--api/views/search.py9
-rw-r--r--api/views/statuses.py139
-rw-r--r--api/views/timelines.py17
9 files changed, 270 insertions, 17 deletions
diff --git a/api/schemas.py b/api/schemas.py
index a8f4e45..97b8169 100644
--- a/api/schemas.py
+++ b/api/schemas.py
@@ -160,3 +160,8 @@ class Relationship(Schema):
domain_blocking: bool
endorsed: bool
note: str
+
+
+class Context(Schema):
+ ancestors: list[Status]
+ descendants: list[Status]
diff --git a/api/views/__init__.py b/api/views/__init__.py
index c6dc765..f95fc21 100644
--- a/api/views/__init__.py
+++ b/api/views/__init__.py
@@ -1,8 +1,10 @@
from .accounts import * # noqa
from .apps import * # noqa
-from .base import api_router # noqa
+from .filters import * # noqa
from .instance import * # noqa
+from .media import * # noqa
from .notifications import * # noqa
from .oauth import * # noqa
from .search import * # noqa
+from .statuses import * # noqa
from .timelines import * # noqa
diff --git a/api/views/accounts.py b/api/views/accounts.py
index 1b883e8..43ec75d 100644
--- a/api/views/accounts.py
+++ b/api/views/accounts.py
@@ -1,12 +1,11 @@
from django.shortcuts import get_object_or_404
-from activities.models import Post
+from activities.models import Post, PostInteraction
from api import schemas
+from api.decorators import identity_required
from api.views.base import api_router
from users.models import Identity
-from ..decorators import identity_required
-
@api_router.get("/v1/accounts/verify_credentials", response=schemas.Account)
@identity_required
@@ -69,7 +68,8 @@ def account_statuses(
):
identity = get_object_or_404(Identity, pk=id)
posts = (
- identity.posts.public()
+ identity.posts.not_hidden()
+ .unlisted(include_replies=not exclude_replies)
.select_related("author")
.prefetch_related("attachments")
.order_by("-created")
@@ -91,4 +91,6 @@ def account_statuses(
# invert the ordering to accomodate
anchor_post = Post.objects.get(pk=min_id)
posts = posts.filter(created__gt=anchor_post.created).order_by("created")
- return [post.to_mastodon_json() for post in posts[:limit]]
+ posts = list(posts[:limit])
+ interactions = PostInteraction.get_post_interactions(posts, request.identity)
+ return [post.to_mastodon_json(interactions=interactions) for post in posts]
diff --git a/api/views/filters.py b/api/views/filters.py
new file mode 100644
index 0000000..ec82a7d
--- /dev/null
+++ b/api/views/filters.py
@@ -0,0 +1,9 @@
+from api.views.base import api_router
+
+from ..decorators import identity_required
+
+
+@api_router.get("/v1/filters")
+@identity_required
+def status(request):
+ return []
diff --git a/api/views/media.py b/api/views/media.py
new file mode 100644
index 0000000..35c0650
--- /dev/null
+++ b/api/views/media.py
@@ -0,0 +1,76 @@
+from django.shortcuts import get_object_or_404
+from ninja import File, Schema
+from ninja.files import UploadedFile
+
+from activities.models import PostAttachment, PostAttachmentStates
+from api import schemas
+from api.views.base import api_router
+from core.files import blurhash_image, resize_image
+
+from ..decorators import identity_required
+
+
+class UploadMediaSchema(Schema):
+ description: str = ""
+ focus: str = "0,0"
+
+
+@api_router.post("/v1/media", response=schemas.MediaAttachment)
+@api_router.post("/v2/media", response=schemas.MediaAttachment)
+@identity_required
+def upload_media(
+ request,
+ file: UploadedFile = File(...),
+ details: UploadMediaSchema | None = None,
+):
+ main_file = resize_image(
+ file,
+ size=(2000, 2000),
+ cover=False,
+ )
+ thumbnail_file = resize_image(
+ file,
+ size=(400, 225),
+ cover=True,
+ )
+ attachment = PostAttachment.objects.create(
+ blurhash=blurhash_image(thumbnail_file),
+ mimetype="image/webp",
+ width=main_file.image.width,
+ height=main_file.image.height,
+ name=details.description if details else None,
+ state=PostAttachmentStates.fetched,
+ )
+ attachment.file.save(
+ main_file.name,
+ main_file,
+ )
+ attachment.thumbnail.save(
+ thumbnail_file.name,
+ thumbnail_file,
+ )
+ attachment.save()
+ return attachment.to_mastodon_json()
+
+
+@api_router.get("/v1/media/{id}", response=schemas.MediaAttachment)
+@identity_required
+def get_media(
+ request,
+ id: str,
+):
+ attachment = get_object_or_404(PostAttachment, pk=id)
+ return attachment.to_mastodon_json()
+
+
+@api_router.put("/v1/media/{id}", response=schemas.MediaAttachment)
+@identity_required
+def update_media(
+ request,
+ id: str,
+ details: UploadMediaSchema | None = None,
+):
+ attachment = get_object_or_404(PostAttachment, pk=id)
+ attachment.name = details.description if details else None
+ attachment.save()
+ return attachment.to_mastodon_json()
diff --git a/api/views/notifications.py b/api/views/notifications.py
index 9ccda81..7b05d14 100644
--- a/api/views/notifications.py
+++ b/api/views/notifications.py
@@ -1,8 +1,7 @@
-from activities.models import TimelineEvent
-
-from .. import schemas
-from ..decorators import identity_required
-from .base import api_router
+from activities.models import PostInteraction, TimelineEvent
+from api import schemas
+from api.decorators import identity_required
+from api.views.base import api_router
@api_router.get("/v1/notifications", response=list[schemas.Notification])
@@ -49,4 +48,9 @@ def notifications(
# invert the ordering to accomodate
anchor_event = TimelineEvent.objects.get(pk=min_id)
events = events.filter(created__gt=anchor_event.created).order_by("created")
- return [event.to_mastodon_notification_json() for event in events[:limit]]
+ events = list(events[:limit])
+ interactions = PostInteraction.get_event_interactions(events, request.identity)
+ return [
+ event.to_mastodon_notification_json(interactions=interactions)
+ for event in events
+ ]
diff --git a/api/views/search.py b/api/views/search.py
index 7735a65..bd44cd7 100644
--- a/api/views/search.py
+++ b/api/views/search.py
@@ -2,6 +2,7 @@ from typing import Literal
from ninja import Field
+from activities.models import PostInteraction
from activities.search import Searcher
from api import schemas
from api.decorators import identity_required
@@ -38,5 +39,11 @@ def search(
if type is None or type == "hashtag":
result["hashtag"] = [h.to_mastodon_json() for h in search_result["hashtags"]]
if type is None or type == "statuses":
- result["statuses"] = [p.to_mastodon_json() for p in search_result["posts"]]
+ interactions = PostInteraction.get_post_interactions(
+ search_result["posts"], request.identity
+ )
+ result["statuses"] = [
+ p.to_mastodon_json(interactions=interactions)
+ for p in search_result["posts"]
+ ]
return result
diff --git a/api/views/statuses.py b/api/views/statuses.py
new file mode 100644
index 0000000..752ee65
--- /dev/null
+++ b/api/views/statuses.py
@@ -0,0 +1,139 @@
+from typing import Literal
+
+from django.forms import ValidationError
+from django.shortcuts import get_object_or_404
+from ninja import Schema
+
+from activities.models import (
+ Post,
+ PostAttachment,
+ PostInteraction,
+ PostStates,
+ TimelineEvent,
+)
+from api import schemas
+from api.views.base import api_router
+from core.models import Config
+
+from ..decorators import identity_required
+
+
+class PostStatusSchema(Schema):
+ status: str
+ in_reply_to_id: str | None = None
+ sensitive: bool = False
+ spoiler_text: str | None = None
+ visibility: Literal["public", "unlisted", "private", "direct"] = "public"
+ language: str | None = None
+ scheduled_at: str | None = None
+ media_ids: list[str] = []
+
+
+@api_router.post("/v1/statuses", response=schemas.Status)
+@identity_required
+def post_status(request, details: PostStatusSchema):
+ # Check text length
+ if len(details.status) > Config.system.post_length:
+ raise ValidationError("Status is too long")
+ if len(details.status) == 0 and not details.media_ids:
+ raise ValidationError("Status is empty")
+ # Grab attachments
+ attachments = [get_object_or_404(PostAttachment, pk=id) for id in details.media_ids]
+ # Create the Post
+ visibility_map = {
+ "public": Post.Visibilities.public,
+ "unlisted": Post.Visibilities.unlisted,
+ "private": Post.Visibilities.followers,
+ "direct": Post.Visibilities.mentioned,
+ }
+ reply_post = None
+ if details.in_reply_to_id:
+ try:
+ reply_post = Post.objects.get(pk=details.in_reply_to_id)
+ except Post.DoesNotExist:
+ pass
+ post = Post.create_local(
+ author=request.identity,
+ content=details.status,
+ summary=details.spoiler_text,
+ sensitive=details.sensitive,
+ visibility=visibility_map[details.visibility],
+ reply_to=reply_post,
+ attachments=attachments,
+ )
+ # Add their own timeline event for immediate visibility
+ TimelineEvent.add_post(request.identity, post)
+ return post.to_mastodon_json()
+
+
+@api_router.get("/v1/statuses/{id}", response=schemas.Status)
+@identity_required
+def status(request, id: str):
+ post = get_object_or_404(Post, pk=id)
+ interactions = PostInteraction.get_post_interactions([post], request.identity)
+ return post.to_mastodon_json(interactions=interactions)
+
+
+@api_router.delete("/v1/statuses/{id}", response=schemas.Status)
+@identity_required
+def delete_status(request, id: str):
+ post = get_object_or_404(Post, pk=id)
+ post.transition_perform(PostStates.deleted)
+ TimelineEvent.objects.filter(subject_post=post, identity=request.identity).delete()
+ return post.to_mastodon_json()
+
+
+@api_router.get("/v1/statuses/{id}/context", response=schemas.Context)
+@identity_required
+def status_context(request, id: str):
+ post = get_object_or_404(Post, pk=id)
+ parent = post.in_reply_to_post()
+ ancestors = []
+ if parent:
+ ancestors.append(parent)
+ descendants = list(Post.objects.filter(in_reply_to=post.object_uri)[:40])
+ interactions = PostInteraction.get_post_interactions(
+ [post] + ancestors + descendants, request.identity
+ )
+ return {
+ "ancestors": [p.to_mastodon_json(interactions=interactions) for p in ancestors],
+ "descendants": [
+ p.to_mastodon_json(interactions=interactions) for p in descendants
+ ],
+ }
+
+
+@api_router.post("/v1/statuses/{id}/favourite", response=schemas.Status)
+@identity_required
+def favourite_status(request, id: str):
+ post = get_object_or_404(Post, pk=id)
+ post.like_as(request.identity)
+ interactions = PostInteraction.get_post_interactions([post], request.identity)
+ return post.to_mastodon_json(interactions=interactions)
+
+
+@api_router.post("/v1/statuses/{id}/unfavourite", response=schemas.Status)
+@identity_required
+def unfavourite_status(request, id: str):
+ post = get_object_or_404(Post, pk=id)
+ post.unlike_as(request.identity)
+ interactions = PostInteraction.get_post_interactions([post], request.identity)
+ return post.to_mastodon_json(interactions=interactions)
+
+
+@api_router.post("/v1/statuses/{id}/reblog", response=schemas.Status)
+@identity_required
+def reblog_status(request, id: str):
+ post = get_object_or_404(Post, pk=id)
+ post.boost_as(request.identity)
+ interactions = PostInteraction.get_post_interactions([post], request.identity)
+ return post.to_mastodon_json(interactions=interactions)
+
+
+@api_router.post("/v1/statuses/{id}/unreblog", response=schemas.Status)
+@identity_required
+def unreblog_status(request, id: str):
+ post = get_object_or_404(Post, pk=id)
+ post.unboost_as(request.identity)
+ interactions = PostInteraction.get_post_interactions([post], request.identity)
+ return post.to_mastodon_json(interactions=interactions)
diff --git a/api/views/timelines.py b/api/views/timelines.py
index d560596..84eed7a 100644
--- a/api/views/timelines.py
+++ b/api/views/timelines.py
@@ -1,4 +1,4 @@
-from activities.models import Post, TimelineEvent
+from activities.models import Post, PostInteraction, TimelineEvent
from .. import schemas
from ..decorators import identity_required
@@ -36,7 +36,12 @@ def home(
# invert the ordering to accomodate
anchor_post = Post.objects.get(pk=min_id)
events = events.filter(created__gt=anchor_post.created).order_by("created")
- return [event.subject_post.to_mastodon_json() for event in events[:limit]]
+ events = list(events[:limit])
+ interactions = PostInteraction.get_event_interactions(events, request.identity)
+ return [
+ event.subject_post.to_mastodon_json(interactions=interactions)
+ for event in events
+ ]
@api_router.get("/v1/timelines/public", response=list[schemas.Status])
@@ -76,7 +81,9 @@ def public(
# invert the ordering to accomodate
anchor_post = Post.objects.get(pk=min_id)
posts = posts.filter(created__gt=anchor_post.created).order_by("created")
- return [post.to_mastodon_json() for post in posts[:limit]]
+ posts = list(posts[:limit])
+ interactions = PostInteraction.get_post_interactions(posts, request.identity)
+ return [post.to_mastodon_json(interactions=interactions) for post in posts]
@api_router.get("/v1/timelines/tag/{hashtag}", response=list[schemas.Status])
@@ -115,7 +122,9 @@ def hashtag(
# invert the ordering to accomodate
anchor_post = Post.objects.get(pk=min_id)
posts = posts.filter(created__gt=anchor_post.created).order_by("created")
- return [post.to_mastodon_json() for post in posts[:limit]]
+ posts = list(posts[:limit])
+ interactions = PostInteraction.get_post_interactions(posts, request.identity)
+ return [post.to_mastodon_json(interactions=interactions) for post in posts]
@api_router.get("/v1/conversations", response=list[schemas.Status])