Skip to content

Commit 4b6746d

Browse files
Add validation for Mastodon handles to prevent invalid URLs
Fixes #3363 Previously, users could enter invalid Mastodon handles like just "username", which resulted in broken URLs like "https://undefined/@username" on the public pages. This change adds validation to enforce proper Mastodon handle formats: - [email protected] - @[email protected] - https://instance.social/@username The validation is applied in: - Participant profile updates (api/participants/mutations.py) - Submission forms for talks/workshops (api/submissions/mutations.py) - Grant applications (api/grants/mutations.py) Also added a test case to verify the validation works correctly. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Marco Acierno <[email protected]>
1 parent 6686fae commit 4b6746d

File tree

4 files changed

+108
-0
lines changed

4 files changed

+108
-0
lines changed

backend/api/grants/mutations.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from enum import Enum
33
from typing import Annotated, Union, Optional
44
from participants.models import Participant
5+
import re
56

67
from privacy_policy.record import record_privacy_policy_acceptance
78
import strawberry
@@ -24,6 +25,12 @@
2425
from grants.tasks import get_name
2526
from notifications.models import EmailTemplate, EmailTemplateIdentifier
2627

28+
FACEBOOK_LINK_MATCH = re.compile(r"^http(s)?:\/\/(www\.)?facebook\.com\/")
29+
LINKEDIN_LINK_MATCH = re.compile(r"^http(s)?:\/\/(www\.)?linkedin\.com\/")
30+
MASTODON_HANDLE_MATCH = re.compile(
31+
r"^(https?:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}\/@[a-zA-Z0-9_]+|@?[a-zA-Z0-9_]+@[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,})$"
32+
)
33+
2734

2835
@strawberry.type
2936
class GrantErrors(BaseErrorType):
@@ -101,6 +108,29 @@ def validate(self, conference: Conference, user: User) -> GrantErrors:
101108
errors.add_error(field, f"{field}: Cannot be empty")
102109
continue
103110

111+
# Validate social media fields
112+
if self.participant_linkedin_url and not LINKEDIN_LINK_MATCH.match(
113+
self.participant_linkedin_url
114+
):
115+
errors.add_error(
116+
"participant_linkedin_url", "Linkedin URL should be a linkedin.com link"
117+
)
118+
119+
if self.participant_facebook_url and not FACEBOOK_LINK_MATCH.match(
120+
self.participant_facebook_url
121+
):
122+
errors.add_error(
123+
"participant_facebook_url", "Facebook URL should be a facebook.com link"
124+
)
125+
126+
if self.participant_mastodon_handle and not MASTODON_HANDLE_MATCH.match(
127+
self.participant_mastodon_handle
128+
):
129+
errors.add_error(
130+
"participant_mastodon_handle",
131+
"Mastodon handle should be in format: [email protected] or @[email protected] or https://instance.social/@username",
132+
)
133+
104134
return errors
105135

106136

backend/api/participants/mutations.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313

1414
FACEBOOK_LINK_MATCH = re.compile(r"^http(s)?:\/\/(www\.)?facebook\.com\/")
1515
LINKEDIN_LINK_MATCH = re.compile(r"^http(s)?:\/\/(www\.)?linkedin\.com\/")
16+
MASTODON_HANDLE_MATCH = re.compile(
17+
r"^(https?:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}\/@[a-zA-Z0-9_]+|@?[a-zA-Z0-9_]+@[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,})$"
18+
)
1619

1720

1821
@strawberry.type
@@ -77,6 +80,14 @@ def validate(self) -> UpdateParticipantErrors:
7780
"facebook_url", "Facebook URL should be a facebook.com link"
7881
)
7982

83+
if self.mastodon_handle and not MASTODON_HANDLE_MATCH.match(
84+
self.mastodon_handle
85+
):
86+
errors.add_error(
87+
"mastodon_handle",
88+
"Mastodon handle should be in format: [email protected] or @[email protected] or https://instance.social/@username",
89+
)
90+
8091
return errors.if_has_errors
8192

8293

backend/api/submissions/mutations.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828

2929
FACEBOOK_LINK_MATCH = re.compile(r"^http(s)?:\/\/(www\.)?facebook\.com\/")
3030
LINKEDIN_LINK_MATCH = re.compile(r"^http(s)?:\/\/(www\.)?linkedin\.com\/")
31+
MASTODON_HANDLE_MATCH = re.compile(
32+
r"^(https?:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}\/@[a-zA-Z0-9_]+|@?[a-zA-Z0-9_]+@[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,})$"
33+
)
3134

3235

3336
@strawberry.type
@@ -197,6 +200,14 @@ def validate(self, conference: Conference):
197200
"speaker_facebook_url", "Facebook URL should be a facebook.com link"
198201
)
199202

203+
if self.speaker_mastodon_handle and not MASTODON_HANDLE_MATCH.match(
204+
self.speaker_mastodon_handle
205+
):
206+
errors.add_error(
207+
"speaker_mastodon_handle",
208+
"Mastodon handle should be in format: [email protected] or @[email protected] or https://instance.social/@username",
209+
)
210+
200211
return errors
201212

202213

backend/api/submissions/tests/test_edit_submission.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ def _update_submission(
132132
validationSpeakerInstagramHandle: speakerInstagramHandle
133133
validationSpeakerLinkedinUrl: speakerLinkedinUrl
134134
validationSpeakerFacebookUrl: speakerFacebookUrl
135+
validationSpeakerMastodonHandle: speakerMastodonHandle
135136
validationMaterials: materials {
136137
fileId
137138
url
@@ -922,6 +923,61 @@ def test_update_submission_with_invalid_linkedin_social_url(graphql_client, user
922923
] == ["Linkedin URL should be a linkedin.com link"]
923924

924925

926+
def test_update_submission_with_invalid_mastodon_handle(graphql_client, user):
927+
conference = ConferenceFactory(
928+
topics=("life", "diy"),
929+
languages=("it", "en"),
930+
durations=("10", "20"),
931+
active_cfp=True,
932+
audience_levels=("adult", "senior"),
933+
submission_types=("talk", "workshop"),
934+
)
935+
936+
submission = SubmissionFactory(
937+
speaker_id=user.id,
938+
custom_topic="life",
939+
custom_duration="10m",
940+
custom_audience_level="adult",
941+
custom_submission_type="talk",
942+
languages=["it"],
943+
tags=["python", "ml"],
944+
conference=conference,
945+
speaker_level=Submission.SPEAKER_LEVELS.intermediate,
946+
previous_talk_video="https://www.youtube.com/watch?v=SlPhMPnQ58k",
947+
)
948+
949+
graphql_client.force_login(user)
950+
951+
new_topic = conference.topics.filter(name="diy").first()
952+
new_audience = conference.audience_levels.filter(name="senior").first()
953+
new_tag = SubmissionTagFactory(name="yello")
954+
new_duration = conference.durations.filter(name="20m").first()
955+
new_type = conference.submission_types.filter(name="workshop").first()
956+
957+
response = _update_submission(
958+
graphql_client,
959+
submission=submission,
960+
new_topic=new_topic,
961+
new_audience=new_audience,
962+
new_tag=new_tag,
963+
new_duration=new_duration,
964+
new_type=new_type,
965+
new_speaker_level=Submission.SPEAKER_LEVELS.experienced,
966+
new_previous_talk_video="https://www.youtube.com/watch?v=dQw4w9WgXcQ",
967+
new_short_social_summary="test",
968+
new_speaker_mastodon_handle="justusername",
969+
)
970+
971+
submission.refresh_from_db()
972+
973+
assert response["data"]["updateSubmission"]["__typename"] == "SendSubmissionErrors"
974+
assert response["data"]["updateSubmission"]["errors"][
975+
"validationSpeakerMastodonHandle"
976+
] == [
977+
"Mastodon handle should be in format: [email protected] or @[email protected] or https://instance.social/@username"
978+
]
979+
980+
925981
def test_update_submission_with_photo_to_upload(
926982
graphql_client,
927983
user,

0 commit comments

Comments
 (0)