mirror of
https://github.com/jointakahe/takahe.git
synced 2024-11-25 08:41:00 +00:00
Basic post mutation
This commit is contained in:
parent
fc8a21fc5c
commit
20239b5cb7
14 changed files with 365 additions and 56 deletions
|
@ -80,6 +80,12 @@ class PostStates(StateGraph):
|
||||||
|
|
||||||
|
|
||||||
class PostQuerySet(models.QuerySet):
|
class PostQuerySet(models.QuerySet):
|
||||||
|
def not_hidden(self):
|
||||||
|
query = self.exclude(
|
||||||
|
state__in=[PostStates.deleted, PostStates.deleted_fanned_out]
|
||||||
|
)
|
||||||
|
return query
|
||||||
|
|
||||||
def public(self, include_replies: bool = False):
|
def public(self, include_replies: bool = False):
|
||||||
query = self.filter(
|
query = self.filter(
|
||||||
visibility__in=[
|
visibility__in=[
|
||||||
|
@ -103,6 +109,18 @@ class PostQuerySet(models.QuerySet):
|
||||||
return query.filter(in_reply_to__isnull=True)
|
return query.filter(in_reply_to__isnull=True)
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
def unlisted(self, include_replies: bool = False):
|
||||||
|
query = self.filter(
|
||||||
|
visibility__in=[
|
||||||
|
Post.Visibilities.public,
|
||||||
|
Post.Visibilities.local_only,
|
||||||
|
Post.Visibilities.unlisted,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
if not include_replies:
|
||||||
|
return query.filter(in_reply_to__isnull=True)
|
||||||
|
return query
|
||||||
|
|
||||||
def tagged_with(self, hashtag: str | Hashtag):
|
def tagged_with(self, hashtag: str | Hashtag):
|
||||||
if isinstance(hashtag, str):
|
if isinstance(hashtag, str):
|
||||||
tag_q = models.Q(hashtags__contains=hashtag)
|
tag_q = models.Q(hashtags__contains=hashtag)
|
||||||
|
@ -118,12 +136,18 @@ class PostManager(models.Manager):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return PostQuerySet(self.model, using=self._db)
|
return PostQuerySet(self.model, using=self._db)
|
||||||
|
|
||||||
|
def not_hidden(self):
|
||||||
|
return self.get_queryset().not_hidden()
|
||||||
|
|
||||||
def public(self, include_replies: bool = False):
|
def public(self, include_replies: bool = False):
|
||||||
return self.get_queryset().public(include_replies=include_replies)
|
return self.get_queryset().public(include_replies=include_replies)
|
||||||
|
|
||||||
def local_public(self, include_replies: bool = False):
|
def local_public(self, include_replies: bool = False):
|
||||||
return self.get_queryset().local_public(include_replies=include_replies)
|
return self.get_queryset().local_public(include_replies=include_replies)
|
||||||
|
|
||||||
|
def unlisted(self, include_replies: bool = False):
|
||||||
|
return self.get_queryset().unlisted(include_replies=include_replies)
|
||||||
|
|
||||||
def tagged_with(self, hashtag: str | Hashtag):
|
def tagged_with(self, hashtag: str | Hashtag):
|
||||||
return self.get_queryset().tagged_with(hashtag=hashtag)
|
return self.get_queryset().tagged_with(hashtag=hashtag)
|
||||||
|
|
||||||
|
@ -248,6 +272,8 @@ class Post(StatorModel):
|
||||||
"""
|
"""
|
||||||
Returns the actual Post object we're replying to, if we can find it
|
Returns the actual Post object we're replying to, if we can find it
|
||||||
"""
|
"""
|
||||||
|
if self.in_reply_to is None:
|
||||||
|
return None
|
||||||
return (
|
return (
|
||||||
Post.objects.filter(object_uri=self.in_reply_to)
|
Post.objects.filter(object_uri=self.in_reply_to)
|
||||||
.select_related("author")
|
.select_related("author")
|
||||||
|
@ -338,6 +364,7 @@ class Post(StatorModel):
|
||||||
author: Identity,
|
author: Identity,
|
||||||
content: str,
|
content: str,
|
||||||
summary: str | None = None,
|
summary: str | None = None,
|
||||||
|
sensitive: bool = False,
|
||||||
visibility: int = Visibilities.public,
|
visibility: int = Visibilities.public,
|
||||||
reply_to: Optional["Post"] = None,
|
reply_to: Optional["Post"] = None,
|
||||||
attachments: list | None = None,
|
attachments: list | None = None,
|
||||||
|
@ -359,7 +386,7 @@ class Post(StatorModel):
|
||||||
author=author,
|
author=author,
|
||||||
content=content,
|
content=content,
|
||||||
summary=summary or None,
|
summary=summary or None,
|
||||||
sensitive=bool(summary),
|
sensitive=bool(summary) or sensitive,
|
||||||
local=True,
|
local=True,
|
||||||
visibility=visibility,
|
visibility=visibility,
|
||||||
hashtags=hashtags,
|
hashtags=hashtags,
|
||||||
|
@ -424,6 +451,48 @@ class Post(StatorModel):
|
||||||
hashtag=hashtag,
|
hashtag=hashtag,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
### Actions ###
|
||||||
|
|
||||||
|
def interact_as(self, identity, type):
|
||||||
|
from activities.models import PostInteraction, PostInteractionStates
|
||||||
|
|
||||||
|
interaction = PostInteraction.objects.get_or_create(
|
||||||
|
type=type, identity=identity, post=self
|
||||||
|
)[0]
|
||||||
|
if interaction.state in [
|
||||||
|
PostInteractionStates.undone,
|
||||||
|
PostInteractionStates.undone_fanned_out,
|
||||||
|
]:
|
||||||
|
interaction.transition_perform(PostInteractionStates.new)
|
||||||
|
|
||||||
|
def uninteract_as(self, identity, type):
|
||||||
|
from activities.models import PostInteraction, PostInteractionStates
|
||||||
|
|
||||||
|
for interaction in PostInteraction.objects.filter(
|
||||||
|
type=type, identity=identity, post=self
|
||||||
|
):
|
||||||
|
interaction.transition_perform(PostInteractionStates.undone)
|
||||||
|
|
||||||
|
def like_as(self, identity):
|
||||||
|
from activities.models import PostInteraction
|
||||||
|
|
||||||
|
self.interact_as(identity, PostInteraction.Types.like)
|
||||||
|
|
||||||
|
def unlike_as(self, identity):
|
||||||
|
from activities.models import PostInteraction
|
||||||
|
|
||||||
|
self.uninteract_as(identity, PostInteraction.Types.like)
|
||||||
|
|
||||||
|
def boost_as(self, identity):
|
||||||
|
from activities.models import PostInteraction
|
||||||
|
|
||||||
|
self.interact_as(identity, PostInteraction.Types.boost)
|
||||||
|
|
||||||
|
def unboost_as(self, identity):
|
||||||
|
from activities.models import PostInteraction
|
||||||
|
|
||||||
|
self.uninteract_as(identity, PostInteraction.Types.boost)
|
||||||
|
|
||||||
### ActivityPub (outbound) ###
|
### ActivityPub (outbound) ###
|
||||||
|
|
||||||
def to_ap(self) -> dict:
|
def to_ap(self) -> dict:
|
||||||
|
@ -711,11 +780,11 @@ class Post(StatorModel):
|
||||||
|
|
||||||
### Mastodon API ###
|
### Mastodon API ###
|
||||||
|
|
||||||
def to_mastodon_json(self):
|
def to_mastodon_json(self, interactions=None):
|
||||||
reply_parent = None
|
reply_parent = None
|
||||||
if self.in_reply_to:
|
if self.in_reply_to:
|
||||||
reply_parent = Post.objects.filter(object_uri=self.in_reply_to).first()
|
reply_parent = Post.objects.filter(object_uri=self.in_reply_to).first()
|
||||||
return {
|
value = {
|
||||||
"id": self.pk,
|
"id": self.pk,
|
||||||
"uri": self.object_uri,
|
"uri": self.object_uri,
|
||||||
"created_at": format_ld_date(self.published),
|
"created_at": format_ld_date(self.published),
|
||||||
|
@ -755,3 +824,7 @@ class Post(StatorModel):
|
||||||
"text": self.safe_content_plain(),
|
"text": self.safe_content_plain(),
|
||||||
"edited_at": format_ld_date(self.edited) if self.edited else None,
|
"edited_at": format_ld_date(self.edited) if self.edited else None,
|
||||||
}
|
}
|
||||||
|
if interactions:
|
||||||
|
value["favourited"] = self.pk in interactions.get("like", [])
|
||||||
|
value["reblogged"] = self.pk in interactions.get("boost", [])
|
||||||
|
return value
|
||||||
|
|
|
@ -148,7 +148,7 @@ class TimelineEvent(models.Model):
|
||||||
|
|
||||||
### Mastodon Client API ###
|
### Mastodon Client API ###
|
||||||
|
|
||||||
def to_mastodon_notification_json(self):
|
def to_mastodon_notification_json(self, interactions=None):
|
||||||
result = {
|
result = {
|
||||||
"id": self.pk,
|
"id": self.pk,
|
||||||
"created_at": format_ld_date(self.created),
|
"created_at": format_ld_date(self.created),
|
||||||
|
@ -156,13 +156,19 @@ class TimelineEvent(models.Model):
|
||||||
}
|
}
|
||||||
if self.type == self.Types.liked:
|
if self.type == self.Types.liked:
|
||||||
result["type"] = "favourite"
|
result["type"] = "favourite"
|
||||||
result["status"] = self.subject_post.to_mastodon_json()
|
result["status"] = self.subject_post.to_mastodon_json(
|
||||||
|
interactions=interactions
|
||||||
|
)
|
||||||
elif self.type == self.Types.boosted:
|
elif self.type == self.Types.boosted:
|
||||||
result["type"] = "reblog"
|
result["type"] = "reblog"
|
||||||
result["status"] = self.subject_post.to_mastodon_json()
|
result["status"] = self.subject_post.to_mastodon_json(
|
||||||
|
interactions=interactions
|
||||||
|
)
|
||||||
elif self.type == self.Types.mentioned:
|
elif self.type == self.Types.mentioned:
|
||||||
result["type"] = "mention"
|
result["type"] = "mention"
|
||||||
result["status"] = self.subject_post.to_mastodon_json()
|
result["status"] = self.subject_post.to_mastodon_json(
|
||||||
|
interactions=interactions
|
||||||
|
)
|
||||||
elif self.type == self.Types.followed:
|
elif self.type == self.Types.followed:
|
||||||
result["type"] = "follow"
|
result["type"] = "follow"
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -6,7 +6,7 @@ from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.vary import vary_on_headers
|
from django.views.decorators.vary import vary_on_headers
|
||||||
from django.views.generic import TemplateView, View
|
from django.views.generic import TemplateView, View
|
||||||
|
|
||||||
from activities.models import Post, PostInteraction, PostInteractionStates, PostStates
|
from activities.models import Post, PostInteraction, PostStates
|
||||||
from core.decorators import cache_page_by_ap_json
|
from core.decorators import cache_page_by_ap_json
|
||||||
from core.ld import canonicalise
|
from core.ld import canonicalise
|
||||||
from users.decorators import identity_required
|
from users.decorators import identity_required
|
||||||
|
@ -94,20 +94,9 @@ class Like(View):
|
||||||
identity.posts.prefetch_related("attachments"), pk=post_id
|
identity.posts.prefetch_related("attachments"), pk=post_id
|
||||||
)
|
)
|
||||||
if self.undo:
|
if self.undo:
|
||||||
# Undo any likes on the post
|
post.unlike_as(self.request.identity)
|
||||||
for interaction in PostInteraction.objects.filter(
|
|
||||||
type=PostInteraction.Types.like,
|
|
||||||
identity=request.identity,
|
|
||||||
post=post,
|
|
||||||
):
|
|
||||||
interaction.transition_perform(PostInteractionStates.undone)
|
|
||||||
else:
|
else:
|
||||||
# Make a like on this post if we didn't already
|
post.like_as(self.request.identity)
|
||||||
PostInteraction.objects.get_or_create(
|
|
||||||
type=PostInteraction.Types.like,
|
|
||||||
identity=request.identity,
|
|
||||||
post=post,
|
|
||||||
)
|
|
||||||
# Return either a redirect or a HTMX snippet
|
# Return either a redirect or a HTMX snippet
|
||||||
if request.htmx:
|
if request.htmx:
|
||||||
return render(
|
return render(
|
||||||
|
@ -133,20 +122,9 @@ class Boost(View):
|
||||||
identity = by_handle_or_404(self.request, handle, local=False)
|
identity = by_handle_or_404(self.request, handle, local=False)
|
||||||
post = get_object_or_404(identity.posts, pk=post_id)
|
post = get_object_or_404(identity.posts, pk=post_id)
|
||||||
if self.undo:
|
if self.undo:
|
||||||
# Undo any boosts on the post
|
post.unboost_as(request.identity)
|
||||||
for interaction in PostInteraction.objects.filter(
|
|
||||||
type=PostInteraction.Types.boost,
|
|
||||||
identity=request.identity,
|
|
||||||
post=post,
|
|
||||||
):
|
|
||||||
interaction.transition_perform(PostInteractionStates.undone)
|
|
||||||
else:
|
else:
|
||||||
# Make a boost on this post if we didn't already
|
post.boost_as(request.identity)
|
||||||
PostInteraction.objects.get_or_create(
|
|
||||||
type=PostInteraction.Types.boost,
|
|
||||||
identity=request.identity,
|
|
||||||
post=post,
|
|
||||||
)
|
|
||||||
# Return either a redirect or a HTMX snippet
|
# Return either a redirect or a HTMX snippet
|
||||||
if request.htmx:
|
if request.htmx:
|
||||||
return render(
|
return render(
|
||||||
|
|
|
@ -160,3 +160,8 @@ class Relationship(Schema):
|
||||||
domain_blocking: bool
|
domain_blocking: bool
|
||||||
endorsed: bool
|
endorsed: bool
|
||||||
note: str
|
note: str
|
||||||
|
|
||||||
|
|
||||||
|
class Context(Schema):
|
||||||
|
ancestors: list[Status]
|
||||||
|
descendants: list[Status]
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
from .accounts import * # noqa
|
from .accounts import * # noqa
|
||||||
from .apps import * # noqa
|
from .apps import * # noqa
|
||||||
from .base import api_router # noqa
|
from .filters import * # noqa
|
||||||
from .instance import * # noqa
|
from .instance import * # noqa
|
||||||
|
from .media import * # noqa
|
||||||
from .notifications import * # noqa
|
from .notifications import * # noqa
|
||||||
from .oauth import * # noqa
|
from .oauth import * # noqa
|
||||||
from .search import * # noqa
|
from .search import * # noqa
|
||||||
|
from .statuses import * # noqa
|
||||||
from .timelines import * # noqa
|
from .timelines import * # noqa
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
from django.shortcuts import get_object_or_404
|
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 import schemas
|
||||||
|
from api.decorators import identity_required
|
||||||
from api.views.base import api_router
|
from api.views.base import api_router
|
||||||
from users.models import Identity
|
from users.models import Identity
|
||||||
|
|
||||||
from ..decorators import identity_required
|
|
||||||
|
|
||||||
|
|
||||||
@api_router.get("/v1/accounts/verify_credentials", response=schemas.Account)
|
@api_router.get("/v1/accounts/verify_credentials", response=schemas.Account)
|
||||||
@identity_required
|
@identity_required
|
||||||
|
@ -69,7 +68,8 @@ def account_statuses(
|
||||||
):
|
):
|
||||||
identity = get_object_or_404(Identity, pk=id)
|
identity = get_object_or_404(Identity, pk=id)
|
||||||
posts = (
|
posts = (
|
||||||
identity.posts.public()
|
identity.posts.not_hidden()
|
||||||
|
.unlisted(include_replies=not exclude_replies)
|
||||||
.select_related("author")
|
.select_related("author")
|
||||||
.prefetch_related("attachments")
|
.prefetch_related("attachments")
|
||||||
.order_by("-created")
|
.order_by("-created")
|
||||||
|
@ -91,4 +91,6 @@ def account_statuses(
|
||||||
# invert the ordering to accomodate
|
# invert the ordering to accomodate
|
||||||
anchor_post = Post.objects.get(pk=min_id)
|
anchor_post = Post.objects.get(pk=min_id)
|
||||||
posts = posts.filter(created__gt=anchor_post.created).order_by("created")
|
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]
|
||||||
|
|
9
api/views/filters.py
Normal file
9
api/views/filters.py
Normal file
|
@ -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 []
|
76
api/views/media.py
Normal file
76
api/views/media.py
Normal file
|
@ -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()
|
|
@ -1,8 +1,7 @@
|
||||||
from activities.models import TimelineEvent
|
from activities.models import PostInteraction, TimelineEvent
|
||||||
|
from api import schemas
|
||||||
from .. import schemas
|
from api.decorators import identity_required
|
||||||
from ..decorators import identity_required
|
from api.views.base import api_router
|
||||||
from .base import api_router
|
|
||||||
|
|
||||||
|
|
||||||
@api_router.get("/v1/notifications", response=list[schemas.Notification])
|
@api_router.get("/v1/notifications", response=list[schemas.Notification])
|
||||||
|
@ -49,4 +48,9 @@ def notifications(
|
||||||
# invert the ordering to accomodate
|
# invert the ordering to accomodate
|
||||||
anchor_event = TimelineEvent.objects.get(pk=min_id)
|
anchor_event = TimelineEvent.objects.get(pk=min_id)
|
||||||
events = events.filter(created__gt=anchor_event.created).order_by("created")
|
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
|
||||||
|
]
|
||||||
|
|
|
@ -2,6 +2,7 @@ from typing import Literal
|
||||||
|
|
||||||
from ninja import Field
|
from ninja import Field
|
||||||
|
|
||||||
|
from activities.models import PostInteraction
|
||||||
from activities.search import Searcher
|
from activities.search import Searcher
|
||||||
from api import schemas
|
from api import schemas
|
||||||
from api.decorators import identity_required
|
from api.decorators import identity_required
|
||||||
|
@ -38,5 +39,11 @@ def search(
|
||||||
if type is None or type == "hashtag":
|
if type is None or type == "hashtag":
|
||||||
result["hashtag"] = [h.to_mastodon_json() for h in search_result["hashtags"]]
|
result["hashtag"] = [h.to_mastodon_json() for h in search_result["hashtags"]]
|
||||||
if type is None or type == "statuses":
|
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
|
return result
|
||||||
|
|
139
api/views/statuses.py
Normal file
139
api/views/statuses.py
Normal file
|
@ -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)
|
|
@ -1,4 +1,4 @@
|
||||||
from activities.models import Post, TimelineEvent
|
from activities.models import Post, PostInteraction, TimelineEvent
|
||||||
|
|
||||||
from .. import schemas
|
from .. import schemas
|
||||||
from ..decorators import identity_required
|
from ..decorators import identity_required
|
||||||
|
@ -36,7 +36,12 @@ def home(
|
||||||
# invert the ordering to accomodate
|
# invert the ordering to accomodate
|
||||||
anchor_post = Post.objects.get(pk=min_id)
|
anchor_post = Post.objects.get(pk=min_id)
|
||||||
events = events.filter(created__gt=anchor_post.created).order_by("created")
|
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])
|
@api_router.get("/v1/timelines/public", response=list[schemas.Status])
|
||||||
|
@ -76,7 +81,9 @@ def public(
|
||||||
# invert the ordering to accomodate
|
# invert the ordering to accomodate
|
||||||
anchor_post = Post.objects.get(pk=min_id)
|
anchor_post = Post.objects.get(pk=min_id)
|
||||||
posts = posts.filter(created__gt=anchor_post.created).order_by("created")
|
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])
|
@api_router.get("/v1/timelines/tag/{hashtag}", response=list[schemas.Status])
|
||||||
|
@ -115,7 +122,9 @@ def hashtag(
|
||||||
# invert the ordering to accomodate
|
# invert the ordering to accomodate
|
||||||
anchor_post = Post.objects.get(pk=min_id)
|
anchor_post = Post.objects.get(pk=min_id)
|
||||||
posts = posts.filter(created__gt=anchor_post.created).order_by("created")
|
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])
|
@api_router.get("/v1/conversations", response=list[schemas.Status])
|
||||||
|
|
|
@ -32,8 +32,8 @@ def resize_image(
|
||||||
return file
|
return file
|
||||||
|
|
||||||
|
|
||||||
def blurhash_image(image) -> str:
|
def blurhash_image(file) -> str:
|
||||||
"""
|
"""
|
||||||
Returns the blurhash for an image
|
Returns the blurhash for an image
|
||||||
"""
|
"""
|
||||||
return blurhash.encode(image, 4, 4)
|
return blurhash.encode(file, 4, 4)
|
||||||
|
|
|
@ -62,9 +62,8 @@ class ViewIdentity(ListView):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
self.identity.posts.filter(
|
self.identity.posts.not_hidden()
|
||||||
visibility__in=[Post.Visibilities.public, Post.Visibilities.unlisted],
|
.unlisted(include_replies=True)
|
||||||
)
|
|
||||||
.select_related("author")
|
.select_related("author")
|
||||||
.prefetch_related("attachments")
|
.prefetch_related("attachments")
|
||||||
.order_by("-created")
|
.order_by("-created")
|
||||||
|
|
Loading…
Reference in a new issue