Fix mastodon pagination to use returned IDs

This commit is contained in:
Andrew Godwin 2023-01-03 01:01:20 -07:00
parent 68e764a36e
commit 252737f846
5 changed files with 79 additions and 44 deletions

View file

@ -1,5 +1,7 @@
import dataclasses import dataclasses
import urllib.parse import urllib.parse
from collections.abc import Callable
from typing import Any
from django.db import models from django.db import models
from django.http import HttpRequest from django.http import HttpRequest
@ -19,6 +21,9 @@ class PaginationResult:
#: The actual applied limit, which may be different from what was requested. #: The actual applied limit, which may be different from what was requested.
limit: int limit: int
#: A list of transformed JSON objects
json_results: list[dict] | None = None
@classmethod @classmethod
def empty(cls): def empty(cls):
return cls(results=[], limit=20) return cls(results=[], limit=20)
@ -29,9 +34,10 @@ class PaginationResult:
""" """
if not self.results: if not self.results:
return None return None
if self.json_results is None:
raise ValueError("You must JSONify the results first")
params = self.filter_params(request, allowed_params) params = self.filter_params(request, allowed_params)
params["max_id"] = self.results[-1].pk params["max_id"] = self.json_results[-1]["id"]
return f"{request.build_absolute_uri(request.path)}?{urllib.parse.urlencode(params)}" return f"{request.build_absolute_uri(request.path)}?{urllib.parse.urlencode(params)}"
@ -41,9 +47,10 @@ class PaginationResult:
""" """
if not self.results: if not self.results:
return None return None
if self.json_results is None:
raise ValueError("You must JSONify the results first")
params = self.filter_params(request, allowed_params) params = self.filter_params(request, allowed_params)
params["min_id"] = self.results[0].pk params["min_id"] = self.json_results[0]["id"]
return f"{request.build_absolute_uri(request.path)}?{urllib.parse.urlencode(params)}" return f"{request.build_absolute_uri(request.path)}?{urllib.parse.urlencode(params)}"
@ -58,6 +65,45 @@ class PaginationResult:
) )
) )
def jsonify_results(self, map_function: Callable[[Any], Any]):
"""
Replaces our results with ones transformed via map_function
"""
self.json_results = [map_function(result) for result in self.results]
def jsonify_posts(self, identity):
"""
Predefined way of JSON-ifying Post objects
"""
interactions = PostInteraction.get_post_interactions(self.results, identity)
self.jsonify_results(
lambda post: post.to_mastodon_json(interactions=interactions)
)
def jsonify_status_events(self, identity):
"""
Predefined way of JSON-ifying TimelineEvent objects representing statuses
"""
interactions = PostInteraction.get_event_interactions(self.results, identity)
self.jsonify_results(
lambda event: event.to_mastodon_status_json(interactions=interactions)
)
def jsonify_notification_events(self, identity):
"""
Predefined way of JSON-ifying TimelineEvent objects representing notifications
"""
interactions = PostInteraction.get_event_interactions(self.results, identity)
self.jsonify_results(
lambda event: event.to_mastodon_notification_json(interactions=interactions)
)
def jsonify_identities(self):
"""
Predefined way of JSON-ifying Identity objects
"""
self.jsonify_results(lambda identity: identity.to_mastodon_json())
@staticmethod @staticmethod
def filter_params(request: HttpRequest, allowed_params: list[str]): def filter_params(request: HttpRequest, allowed_params: list[str]):
params = {} params = {}

View file

@ -3,7 +3,7 @@ from django.http import HttpRequest, HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from ninja import Field from ninja import Field
from activities.models import Post, PostInteraction 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
@ -143,7 +143,7 @@ def account_statuses(
queryset = queryset.filter(attachments__pk__isnull=False) queryset = queryset.filter(attachments__pk__isnull=False)
if tagged: if tagged:
queryset = queryset.tagged_with(tagged) queryset = queryset.tagged_with(tagged)
# Get user posts with pagination
paginator = MastodonPaginator(Post, sort_attribute="published") paginator = MastodonPaginator(Post, sort_attribute="published")
pager = paginator.paginate( pager = paginator.paginate(
queryset, queryset,
@ -152,7 +152,9 @@ def account_statuses(
since_id=since_id, since_id=since_id,
limit=limit, limit=limit,
) )
# Convert those to the JSON form
pager.jsonify_posts(identity=request.identity)
# Add a link header if we need to
if pager.results: if pager.results:
response.headers["Link"] = pager.link_header( response.headers["Link"] = pager.link_header(
request, request,
@ -166,11 +168,7 @@ def account_statuses(
"tagged", "tagged",
], ],
) )
return pager.json_results
interactions = PostInteraction.get_post_interactions(
pager.results, request.identity
)
return [post.to_mastodon_json(interactions=interactions) for post in pager.results]
@api_router.post("/v1/accounts/{id}/follow", response=schemas.Relationship) @api_router.post("/v1/accounts/{id}/follow", response=schemas.Relationship)
@ -222,6 +220,7 @@ def account_following(
since_id=since_id, since_id=since_id,
limit=limit, limit=limit,
) )
pager.jsonify_identities()
if pager.results: if pager.results:
response.headers["Link"] = pager.link_header( response.headers["Link"] = pager.link_header(
@ -229,4 +228,4 @@ def account_following(
["limit"], ["limit"],
) )
return [result.to_mastodon_json() for result in pager.results] return pager.json_results

View file

@ -1,6 +1,6 @@
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from activities.models import PostInteraction, TimelineEvent from activities.models import TimelineEvent
from activities.services import TimelineService from activities.services import TimelineService
from api import schemas from api import schemas
from api.decorators import identity_required from api.decorators import identity_required
@ -43,14 +43,9 @@ def notifications(
since_id=since_id, since_id=since_id,
limit=limit, limit=limit,
) )
pager.jsonify_notification_events(identity=request.identity)
if pager.results: if pager.results:
response.headers["Link"] = pager.link_header(request, ["limit", "account_id"]) response.headers["Link"] = pager.link_header(request, ["limit", "account_id"])
interactions = PostInteraction.get_event_interactions( return pager.json_results
pager.results, request.identity
)
return [
event.to_mastodon_notification_json(interactions=interactions)
for event in pager.results
]

View file

@ -155,6 +155,7 @@ def favourited_by(
since_id=since_id, since_id=since_id,
limit=limit, limit=limit,
) )
pager.jsonify_identities()
if pager.results: if pager.results:
response.headers["Link"] = pager.link_header( response.headers["Link"] = pager.link_header(
@ -162,7 +163,7 @@ def favourited_by(
["limit"], ["limit"],
) )
return [result.identity.to_mastodon_json() for result in pager.results] return pager.json_results
@api_router.post("/v1/statuses/{id}/reblog", response=schemas.Status) @api_router.post("/v1/statuses/{id}/reblog", response=schemas.Status)

View file

@ -1,6 +1,6 @@
from django.http import HttpRequest, HttpResponse, JsonResponse from django.http import HttpRequest, HttpResponse, JsonResponse
from activities.models import Post, PostInteraction from activities.models import Post
from activities.services import TimelineService from activities.services import TimelineService
from api import schemas from api import schemas
from api.decorators import identity_required from api.decorators import identity_required
@ -19,6 +19,7 @@ def home(
min_id: str | None = None, min_id: str | None = None,
limit: int = 20, limit: int = 20,
): ):
# Grab a paginated result set of instances
paginator = MastodonPaginator(Post, sort_attribute="published") paginator = MastodonPaginator(Post, sort_attribute="published")
queryset = TimelineService(request.identity).home() queryset = TimelineService(request.identity).home()
pager = paginator.paginate( pager = paginator.paginate(
@ -28,17 +29,12 @@ def home(
since_id=since_id, since_id=since_id,
limit=limit, limit=limit,
) )
interactions = PostInteraction.get_event_interactions( # Convert those to the JSON form
pager.results, request.identity pager.jsonify_status_events(identity=request.identity)
) # Add the link header if needed
if pager.results: if pager.results:
response.headers["Link"] = pager.link_header(request, ["limit"]) response.headers["Link"] = pager.link_header(request, ["limit"])
return pager.json_results
return [
event.to_mastodon_status_json(interactions=interactions)
for event in pager.results
]
@api_router.get("/v1/timelines/public", response=list[schemas.Status]) @api_router.get("/v1/timelines/public", response=list[schemas.Status])
@ -64,6 +60,7 @@ def public(
queryset = queryset.filter(local=False) queryset = queryset.filter(local=False)
if only_media: if only_media:
queryset = queryset.filter(attachments__id__isnull=True) queryset = queryset.filter(attachments__id__isnull=True)
# Grab a paginated result set of instances
paginator = MastodonPaginator(Post, sort_attribute="published") paginator = MastodonPaginator(Post, sort_attribute="published")
pager = paginator.paginate( pager = paginator.paginate(
queryset, queryset,
@ -72,17 +69,15 @@ def public(
since_id=since_id, since_id=since_id,
limit=limit, limit=limit,
) )
# Convert those to the JSON form
pager.jsonify_posts(identity=request.identity)
# Add the link header if needed
if pager.results: if pager.results:
response.headers["Link"] = pager.link_header( response.headers["Link"] = pager.link_header(
request, request,
["limit", "local", "remote", "only_media"], ["limit", "local", "remote", "only_media"],
) )
return pager.json_results
interactions = PostInteraction.get_post_interactions(
pager.results, request.identity
)
return [post.to_mastodon_json(interactions=interactions) for post in pager.results]
@api_router.get("/v1/timelines/tag/{hashtag}", response=list[schemas.Status]) @api_router.get("/v1/timelines/tag/{hashtag}", response=list[schemas.Status])
@ -105,6 +100,7 @@ def hashtag(
queryset = queryset.filter(local=True) queryset = queryset.filter(local=True)
if only_media: if only_media:
queryset = queryset.filter(attachments__id__isnull=True) queryset = queryset.filter(attachments__id__isnull=True)
# Grab a paginated result set of instances
paginator = MastodonPaginator(Post, sort_attribute="published") paginator = MastodonPaginator(Post, sort_attribute="published")
pager = paginator.paginate( pager = paginator.paginate(
queryset, queryset,
@ -113,17 +109,15 @@ def hashtag(
since_id=since_id, since_id=since_id,
limit=limit, limit=limit,
) )
# Convert those to the JSON form
pager.jsonify_posts(identity=request.identity)
# Add a link header if we need to
if pager.results: if pager.results:
response.headers["Link"] = pager.link_header( response.headers["Link"] = pager.link_header(
request, request,
["limit", "local", "remote", "only_media"], ["limit", "local", "remote", "only_media"],
) )
return pager.json_results
interactions = PostInteraction.get_post_interactions(
pager.results, request.identity
)
return [post.to_mastodon_json(interactions=interactions) for post in pager.results]
@api_router.get("/v1/conversations", response=list[schemas.Status]) @api_router.get("/v1/conversations", response=list[schemas.Status])