Implement API follow/unfollow

Fixes #198
This commit is contained in:
Andrew Godwin 2022-12-19 20:54:09 +00:00
parent c7409b3500
commit 0dca7eae5f
4 changed files with 98 additions and 30 deletions

View File

@ -6,6 +6,7 @@ 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 users.models import Identity from users.models import Identity
from users.services import IdentityService
@api_router.get("/v1/accounts/verify_credentials", response=schemas.Account) @api_router.get("/v1/accounts/verify_credentials", response=schemas.Account)
@ -22,25 +23,7 @@ def account_relationships(request):
for id in ids: for id in ids:
identity = get_object_or_404(Identity, pk=id) identity = get_object_or_404(Identity, pk=id)
result.append( result.append(
{ IdentityService(identity).mastodon_json_relationship(request.identity)
"id": identity.pk,
"following": identity.inbound_follows.filter(
source=request.identity
).exists(),
"followed_by": identity.outbound_follows.filter(
target=request.identity
).exists(),
"showing_reblogs": True,
"notifying": False,
"blocking": False,
"blocked_by": False,
"muting": False,
"muting_notifications": False,
"requested": False,
"domain_blocking": False,
"endorsed": False,
"note": "",
}
) )
return result return result
@ -95,3 +78,25 @@ def account_statuses(
) )
interactions = PostInteraction.get_post_interactions(posts, request.identity) interactions = PostInteraction.get_post_interactions(posts, request.identity)
return [post.to_mastodon_json(interactions=interactions) for post in queryset] return [post.to_mastodon_json(interactions=interactions) for post in queryset]
@api_router.post("/v1/accounts/{id}/follow", response=schemas.Relationship)
@identity_required
def account_follow(request, id: str):
identity = get_object_or_404(
Identity.objects.exclude(restriction=Identity.Restriction.blocked), pk=id
)
service = IdentityService(identity)
service.follow_from(request.identity)
return service.mastodon_json_relationship(request.identity)
@api_router.post("/v1/accounts/{id}/unfollow", response=schemas.Relationship)
@identity_required
def account_unfollow(request, id: str):
identity = get_object_or_404(
Identity.objects.exclude(restriction=Identity.Restriction.blocked), pk=id
)
service = IdentityService(identity)
service.unfollow_from(request.identity)
return service.mastodon_json_relationship(request.identity)

View File

@ -0,0 +1 @@
from .identity import IdentityService # noqa

View File

@ -0,0 +1,70 @@
from typing import cast
from users.models import Follow, FollowStates, Identity
class IdentityService:
"""
High-level helper methods for doing things to identities
"""
def __init__(self, identity: Identity):
self.identity = identity
def follow_from(self, from_identity: Identity) -> Follow:
"""
Follows a user (or does nothing if already followed).
Returns the follow.
"""
existing_follow = Follow.maybe_get(from_identity, self.identity)
if not existing_follow:
Follow.create_local(from_identity, self.identity)
elif existing_follow.state in [
FollowStates.undone,
FollowStates.undone_remotely,
]:
existing_follow.transition_perform(FollowStates.unrequested)
return cast(Follow, existing_follow)
def unfollow_from(self, from_identity: Identity):
"""
Unfollows a user (or does nothing if not followed).
"""
existing_follow = Follow.maybe_get(from_identity, self.identity)
if existing_follow:
existing_follow.transition_perform(FollowStates.undone)
def mastodon_json_relationship(self, from_identity: Identity):
"""
Returns a Relationship object for the from_identity's relationship
with this identity.
"""
return {
"id": self.identity.pk,
"following": self.identity.inbound_follows.filter(source=from_identity)
.exclude(
state__in=[
FollowStates.undone,
FollowStates.undone_remotely,
]
)
.exists(),
"followed_by": self.identity.outbound_follows.filter(target=from_identity)
.exclude(
state__in=[
FollowStates.undone,
FollowStates.undone_remotely,
]
)
.exists(),
"showing_reblogs": True,
"notifying": False,
"blocking": False,
"blocked_by": False,
"muting": False,
"muting_notifications": False,
"requested": False,
"domain_blocking": False,
"endorsed": False,
"note": "",
}

View File

@ -16,6 +16,7 @@ from core.ld import canonicalise
from core.models import Config from core.models import Config
from users.decorators import identity_required from users.decorators import identity_required
from users.models import Domain, Follow, FollowStates, Identity, IdentityStates from users.models import Domain, Follow, FollowStates, Identity, IdentityStates
from users.services import IdentityService
from users.shortcuts import by_handle_or_404 from users.shortcuts import by_handle_or_404
@ -146,18 +147,9 @@ class ActionIdentity(View):
# See what action we should perform # See what action we should perform
action = self.request.POST["action"] action = self.request.POST["action"]
if action == "follow": if action == "follow":
existing_follow = Follow.maybe_get(self.request.identity, identity) IdentityService(identity).follow_from(self.request.identity)
if not existing_follow:
Follow.create_local(self.request.identity, identity)
elif existing_follow.state in [
FollowStates.undone,
FollowStates.undone_remotely,
]:
existing_follow.transition_perform(FollowStates.unrequested)
elif action == "unfollow": elif action == "unfollow":
existing_follow = Follow.maybe_get(self.request.identity, identity) IdentityService(identity).unfollow_from(self.request.identity)
if existing_follow:
existing_follow.transition_perform(FollowStates.undone)
else: else:
raise ValueError(f"Cannot handle identity action {action}") raise ValueError(f"Cannot handle identity action {action}")
return redirect(identity.urls.view) return redirect(identity.urls.view)