Skip to content

Commit

Permalink
Support follow requests (jointakahe#625)
Browse files Browse the repository at this point in the history
  • Loading branch information
alphatownsman authored Aug 18, 2023
1 parent faa1818 commit 70b9e3b
Show file tree
Hide file tree
Showing 11 changed files with 283 additions and 99 deletions.
1 change: 1 addition & 0 deletions activities/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ class Migration(migrations.Migration):
("mentioned", "Mentioned"),
("liked", "Liked"),
("followed", "Followed"),
("follow_requested", "Follow Requested"),
("boosted", "Boosted"),
("announcement", "Announcement"),
("identity_created", "Identity Created"),
Expand Down
29 changes: 28 additions & 1 deletion activities/models/timeline_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class Types(models.TextChoices):
mentioned = "mentioned"
liked = "liked" # Someone liking one of our posts
followed = "followed"
follow_requested = "follow_requested"
boosted = "boosted" # Someone boosting one of our posts
announcement = "announcement" # Server announcement
identity_created = "identity_created" # New identity created
Expand Down Expand Up @@ -74,14 +75,30 @@ class Meta:
@classmethod
def add_follow(cls, identity, source_identity):
"""
Adds a follow to the timeline if it's not there already
Adds a follow to the timeline if it's not there already, remove follow request if any
"""
cls.objects.filter(
type=cls.Types.follow_requested,
identity=identity,
subject_identity=source_identity,
).delete()
return cls.objects.get_or_create(
identity=identity,
type=cls.Types.followed,
subject_identity=source_identity,
)[0]

@classmethod
def add_follow_request(cls, identity, source_identity):
"""
Adds a follow request to the timeline if it's not there already
"""
return cls.objects.get_or_create(
identity=identity,
type=cls.Types.follow_requested,
subject_identity=source_identity,
)[0]

@classmethod
def add_post(cls, identity, post):
"""
Expand Down Expand Up @@ -169,6 +186,14 @@ def delete_post_interaction(cls, identity, interaction):
subject_identity_id=interaction.identity_id,
).delete()

@classmethod
def delete_follow(cls, target, source):
TimelineEvent.objects.filter(
type__in=[cls.Types.followed, cls.Types.follow_requested],
identity=target,
subject_identity=source,
).delete()

### Background tasks ###

@classmethod
Expand Down Expand Up @@ -218,6 +243,8 @@ def to_mastodon_notification_json(self, interactions=None):
)
elif self.type == self.Types.followed:
result["type"] = "follow"
elif self.type == self.Types.follow_requested:
result["type"] = "follow_request"
elif self.type == self.Types.identity_created:
result["type"] = "admin.sign_up"
else:
Expand Down
2 changes: 2 additions & 0 deletions api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@
path("v1/filters", filters.list_filters),
# Follow requests
path("v1/follow_requests", follow_requests.follow_requests),
path("v1/follow_requests/<id>/authorize", follow_requests.accept_follow_request),
path("v1/follow_requests/<id>/reject", follow_requests.reject_follow_request),
# Instance
path("v1/instance", instance.instance_info_v1),
path("v1/instance/activity", instance.activity),
Expand Down
3 changes: 3 additions & 0 deletions api/views/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def update_credentials(
display_name: QueryOrBody[str | None] = None,
note: QueryOrBody[str | None] = None,
discoverable: QueryOrBody[bool | None] = None,
locked: QueryOrBody[bool | None] = None,
source: QueryOrBody[dict[str, Any] | None] = None,
fields_attributes: QueryOrBody[dict[str, dict[str, str]] | None] = None,
avatar: File | None = None,
Expand All @@ -42,6 +43,8 @@ def update_credentials(
service.set_summary(note)
if discoverable is not None:
identity.discoverable = discoverable
if locked is not None:
identity.manually_approves_followers = locked
if source:
if "privacy" in source:
privacy_map = {
Expand Down
46 changes: 44 additions & 2 deletions api/views/follow_requests.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from django.http import HttpRequest
from django.shortcuts import get_object_or_404
from hatchway import api_view

from api import schemas
from api.decorators import scope_required
from api.pagination import MastodonPaginator, PaginatingApiResponse, PaginationResult
from users.models.identity import Identity
from users.services.identity import IdentityService


@scope_required("read:follows")
Expand All @@ -14,5 +18,43 @@ def follow_requests(
min_id: str | None = None,
limit: int = 40,
) -> list[schemas.Account]:
# We don't implement this yet
return []
service = IdentityService(request.identity)
paginator = MastodonPaginator(max_limit=80)
pager: PaginationResult[Identity] = paginator.paginate(
service.follow_requests(),
min_id=min_id,
max_id=max_id,
since_id=since_id,
limit=limit,
)
return PaginatingApiResponse(
[schemas.Account.from_identity(i) for i in pager.results],
request=request,
include_params=["limit"],
)


@scope_required("write:follows")
@api_view.post
def accept_follow_request(
request: HttpRequest,
id: str | None = None,
) -> schemas.Relationship:
source_identity = get_object_or_404(
Identity.objects.exclude(restriction=Identity.Restriction.blocked), pk=id
)
IdentityService(request.identity).accept_follow_request(source_identity)
return IdentityService(source_identity).mastodon_json_relationship(request.identity)


@scope_required("write:follows")
@api_view.post
def reject_follow_request(
request: HttpRequest,
id: str | None = None,
) -> schemas.Relationship:
source_identity = get_object_or_404(
Identity.objects.exclude(restriction=Identity.Restriction.blocked), pk=id
)
IdentityService(request.identity).reject_follow_request(source_identity)
return IdentityService(source_identity).mastodon_json_relationship(request.identity)
3 changes: 2 additions & 1 deletion tests/activities/models/test_timeline_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,8 @@ def test_clear_timeline(
else:
service.unfollow(remote_identity)

# Run stator once to process the timeline clear message
# Run stator twice to process the timeline clear message
stator.run_single_cycle()
stator.run_single_cycle()

# Verify that the right things vanished
Expand Down
3 changes: 2 additions & 1 deletion tests/users/models/test_follow.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def test_follow(
assert outbound_data["actor"] == identity.actor_uri
assert outbound_data["object"] == remote_identity.actor_uri
assert outbound_data["id"] == f"{identity.actor_uri}follow/{follow.pk}/"
assert Follow.objects.get(pk=follow.pk).state == FollowStates.local_requested
assert Follow.objects.get(pk=follow.pk).state == FollowStates.pending_approval
# Come in with an inbox message of either a reference type or an embedded type
if ref_only:
message = {
Expand All @@ -53,4 +53,5 @@ def test_follow(
InboxMessage.objects.create(message=message)
# Run stator and ensure that accepted our follow
stator.run_single_cycle()
stator.run_single_cycle()
assert Follow.objects.get(pk=follow.pk).state == FollowStates.accepted
22 changes: 22 additions & 0 deletions users/migrations/0022_follow_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 4.2.3 on 2023-08-04 01:38

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("users", "0021_identity_aliases"),
]

operations = [
migrations.RunSQL(
"UPDATE users_follow SET state = 'pending_approval' WHERE state = 'local_requested';"
),
migrations.RunSQL(
"UPDATE users_follow SET state = 'accepting' WHERE state = 'remote_requested';"
),
migrations.RunSQL(
"DELETE FROM users_follow WHERE state not in ('accepted', 'accepting', 'pending_approval', 'unrequested');"
),
]
Loading

0 comments on commit 70b9e3b

Please sign in to comment.