From b31c5156ffd2177cf4d87f0f503fe661f64fce03 Mon Sep 17 00:00:00 2001 From: Christof Dorner Date: Thu, 6 Apr 2023 21:14:21 +0000 Subject: [PATCH] Improve hashtag case handling and implement /api/v1/tags/ endpoint (#554) * Lowercase hashtag before loading its timeline * Implement /api/v1/tags/ endpoint * Lower hashtag before un-/following * Fix field name for hashtag following/followed boolean --- activities/models/hashtag.py | 6 +++--- api/schemas.py | 8 ++++---- api/urls.py | 1 + api/views/tags.py | 24 ++++++++++++++++++++---- api/views/timelines.py | 2 +- 5 files changed, 29 insertions(+), 12 deletions(-) diff --git a/activities/models/hashtag.py b/activities/models/hashtag.py index e5fc55f..3a212e4 100644 --- a/activities/models/hashtag.py +++ b/activities/models/hashtag.py @@ -168,14 +168,14 @@ class Hashtag(StatorModel): results[date(year, month, day)] = val return dict(sorted(results.items(), reverse=True)[:num]) - def to_mastodon_json(self, followed: bool | None = None): + def to_mastodon_json(self, following: bool | None = None): value = { "name": self.hashtag, "url": self.urls.view.full(), # type: ignore "history": [], } - if followed is not None: - value["followed"] = followed + if following is not None: + value["following"] = following return value diff --git a/api/schemas.py b/api/schemas.py index 83a252f..7498d64 100644 --- a/api/schemas.py +++ b/api/schemas.py @@ -276,15 +276,15 @@ class Tag(Schema): name: str url: str history: dict - followed: bool | None + following: bool | None @classmethod def from_hashtag( cls, hashtag: activities_models.Hashtag, - followed: bool | None = None, + following: bool | None = None, ) -> "Tag": - return cls(**hashtag.to_mastodon_json(followed=followed)) + return cls(**hashtag.to_mastodon_json(following=following)) class FollowedTag(Tag): @@ -295,7 +295,7 @@ class FollowedTag(Tag): cls, follow: users_models.HashtagFollow, ) -> "FollowedTag": - return cls(id=follow.id, **follow.hashtag.to_mastodon_json(followed=True)) + return cls(id=follow.id, **follow.hashtag.to_mastodon_json(following=True)) @classmethod def map_from_follows( diff --git a/api/urls.py b/api/urls.py index ab014ea..eb3f31a 100644 --- a/api/urls.py +++ b/api/urls.py @@ -96,6 +96,7 @@ urlpatterns = [ path("v1/statuses//unbookmark", statuses.unbookmark_status), # Tags path("v1/followed_tags", tags.followed_tags), + path("v1/tags/", tags.hashtag), path("v1/tags//follow", tags.follow), path("v1/tags//unfollow", tags.unfollow), # Timelines diff --git a/api/views/tags.py b/api/views/tags.py index 16a63e1..8633b81 100644 --- a/api/views/tags.py +++ b/api/views/tags.py @@ -9,6 +9,22 @@ from api.pagination import MastodonPaginator, PaginatingApiResponse, PaginationR from users.models import HashtagFollow +@api_view.get +def hashtag(request: HttpRequest, hashtag: str) -> schemas.Tag: + tag = get_object_or_404( + Hashtag, + pk=hashtag.lower(), + ) + following = None + if request.identity: + following = tag.followers.filter(identity=request.identity).exists() + + return schemas.Tag.from_hashtag( + tag, + following=following, + ) + + @scope_required("read:follows") @api_view.get def followed_tags( @@ -42,12 +58,12 @@ def follow( ) -> schemas.Tag: hashtag = get_object_or_404( Hashtag, - pk=id, + pk=id.lower(), ) request.identity.hashtag_follows.get_or_create(hashtag=hashtag) return schemas.Tag.from_hashtag( hashtag, - followed=True, + following=True, ) @@ -59,10 +75,10 @@ def unfollow( ) -> schemas.Tag: hashtag = get_object_or_404( Hashtag, - pk=id, + pk=id.lower(), ) request.identity.hashtag_follows.filter(hashtag=hashtag).delete() return schemas.Tag.from_hashtag( hashtag, - followed=False, + following=False, ) diff --git a/api/views/timelines.py b/api/views/timelines.py index d8101cf..9f4ed5a 100644 --- a/api/views/timelines.py +++ b/api/views/timelines.py @@ -101,7 +101,7 @@ def hashtag( ) -> ApiResponse[list[schemas.Status]]: if limit > 40: limit = 40 - queryset = TimelineService(request.identity).hashtag(hashtag) + queryset = TimelineService(request.identity).hashtag(hashtag.lower()) if local: queryset = queryset.filter(local=True) if only_media: