diff --git a/activities/models/hashtag.py b/activities/models/hashtag.py index 3a212e4..894973c 100644 --- a/activities/models/hashtag.py +++ b/activities/models/hashtag.py @@ -87,6 +87,8 @@ class HashtagManager(models.Manager): class Hashtag(StatorModel): + MAXIMUM_LENGTH = 100 + # Normalized hashtag without the '#' hashtag = models.SlugField(primary_key=True, max_length=100) diff --git a/activities/models/post.py b/activities/models/post.py index aaf2a83..6f2b184 100644 --- a/activities/models/post.py +++ b/activities/models/post.py @@ -489,7 +489,10 @@ class Post(StatorModel): # Strip all unwanted HTML and apply linebreaks filter, grabbing hashtags on the way parser = FediverseHtmlParser(linebreaks_filter(content), find_hashtags=True) content = parser.html - hashtags = sorted(parser.hashtags) or None + hashtags = ( + sorted([tag[: Hashtag.MAXIMUM_LENGTH] for tag in parser.hashtags]) + or None + ) # Make the Post object post = cls.objects.create( author=author, @@ -529,7 +532,10 @@ class Post(StatorModel): # Strip all HTML and apply linebreaks filter parser = FediverseHtmlParser(linebreaks_filter(content), find_hashtags=True) self.content = parser.html - self.hashtags = sorted(parser.hashtags) or None + self.hashtags = ( + sorted([tag[: Hashtag.MAXIMUM_LENGTH] for tag in parser.hashtags]) + or None + ) self.summary = summary or None self.sensitive = bool(summary) if sensitive is None else sensitive self.visibility = visibility @@ -577,7 +583,7 @@ class Post(StatorModel): if self.hashtags: for hashtag in self.hashtags: tag, _ = await Hashtag.objects.aget_or_create( - hashtag=hashtag, + hashtag=hashtag[: Hashtag.MAXIMUM_LENGTH], ) await tag.atransition_perform(HashtagStates.outdated) @@ -876,7 +882,9 @@ class Post(StatorModel): post.mentions.add(mention_identity) elif tag_type in ["_:hashtag", "hashtag"]: post.hashtags.append( - get_value_or_map(tag, "name", "nameMap").lower().lstrip("#") + get_value_or_map(tag, "name", "nameMap") + .lower() + .lstrip("#")[: Hashtag.MAXIMUM_LENGTH] ) elif tag_type in ["toot:emoji", "emoji"]: emoji = Emoji.by_ap_tag(post.author.domain, tag, create=True) diff --git a/tests/activities/models/test_post.py b/tests/activities/models/test_post.py index 6688ef6..58d5e05 100644 --- a/tests/activities/models/test_post.py +++ b/tests/activities/models/test_post.py @@ -1,7 +1,7 @@ import pytest from pytest_httpx import HTTPXMock -from activities.models import Post, PostStates +from activities.models import Hashtag, Post, PostStates from activities.models.post_types import QuestionData from users.models import Identity, InboxMessage @@ -57,6 +57,34 @@ def test_post_create_edit(identity: Identity, config_system): assert list(post.mentions.all()) == [] +@pytest.mark.django_db +def test_ensure_hashtag(identity: Identity, config_system, stator): + """ + Tests that normal hashtags get a Hashtag object created, and a hashtag + over our limit of 100 characters is truncated. + """ + # Normal length hashtag + post = Post.create_local( + author=identity, + content="Hello, #testtag", + ) + stator.run_single_cycle_sync() + assert post.hashtags == ["testtag"] + assert Hashtag.objects.filter(hashtag="testtag").exists() + # Excessively long hashtag + post = Post.create_local( + author=identity, + content="Hello, #thisisahashtagthatiswaytoolongandissignificantlyaboveourmaximumlimitofonehundredcharacterswhytheywouldbethislongidontknow", + ) + stator.run_single_cycle_sync() + assert post.hashtags == [ + "thisisahashtagthatiswaytoolongandissignificantlyaboveourmaximumlimitofonehundredcharacterswhytheywou" + ] + assert Hashtag.objects.filter( + hashtag="thisisahashtagthatiswaytoolongandissignificantlyaboveourmaximumlimitofonehundredcharacterswhytheywou" + ).exists() + + @pytest.mark.django_db def test_linkify_mentions_remote( identity, identity2, remote_identity, remote_identity2