126 lines
3.4 KiB
Python
126 lines
3.4 KiB
Python
"""Validation utilities for user profile fields."""
|
|
from email_validator import validate_email, EmailNotValidError
|
|
from bech32 import bech32_decode
|
|
|
|
|
|
def validate_contact_email(value: str | None) -> str | None:
|
|
"""
|
|
Validate contact email format.
|
|
|
|
Returns None if valid, error message if invalid.
|
|
Empty/None values are valid (field is optional).
|
|
"""
|
|
if not value:
|
|
return None
|
|
|
|
try:
|
|
validate_email(value, check_deliverability=False)
|
|
return None
|
|
except EmailNotValidError as e:
|
|
return str(e)
|
|
|
|
|
|
def validate_telegram(value: str | None) -> str | None:
|
|
"""
|
|
Validate Telegram handle.
|
|
|
|
Must start with @ if provided, with 1-32 characters after @.
|
|
Returns None if valid, error message if invalid.
|
|
Empty/None values are valid (field is optional).
|
|
"""
|
|
if not value:
|
|
return None
|
|
|
|
if not value.startswith("@"):
|
|
return "Telegram handle must start with @"
|
|
|
|
handle = value[1:] # Remove @
|
|
if len(handle) < 1:
|
|
return "Telegram handle must have at least one character after @"
|
|
|
|
if len(handle) > 32:
|
|
return "Telegram handle must be at most 32 characters (after @)"
|
|
|
|
return None
|
|
|
|
|
|
def validate_signal(value: str | None) -> str | None:
|
|
"""
|
|
Validate Signal username.
|
|
|
|
Any non-empty string is valid.
|
|
Returns None if valid, error message if invalid.
|
|
Empty/None values are valid (field is optional).
|
|
"""
|
|
if not value:
|
|
return None
|
|
|
|
# Signal usernames are fairly permissive, just check it's not empty
|
|
if len(value.strip()) == 0:
|
|
return "Signal username cannot be empty"
|
|
|
|
if len(value) > 64:
|
|
return "Signal username must be at most 64 characters"
|
|
|
|
return None
|
|
|
|
|
|
def validate_nostr_npub(value: str | None) -> str | None:
|
|
"""
|
|
Validate Nostr npub (public key in bech32 format).
|
|
|
|
Must be valid bech32 with 'npub' prefix.
|
|
Returns None if valid, error message if invalid.
|
|
Empty/None values are valid (field is optional).
|
|
"""
|
|
if not value:
|
|
return None
|
|
|
|
if not value.startswith("npub1"):
|
|
return "Nostr npub must start with 'npub1'"
|
|
|
|
# Decode bech32 to validate checksum
|
|
hrp, data = bech32_decode(value)
|
|
|
|
if hrp is None or data is None:
|
|
return "Invalid Nostr npub: bech32 checksum failed"
|
|
|
|
if hrp != "npub":
|
|
return "Nostr npub must have 'npub' prefix"
|
|
|
|
# npub should decode to 32 bytes (256 bits) for a public key
|
|
# In bech32, each character encodes 5 bits, so 32 bytes = 52 characters of data
|
|
if len(data) != 52:
|
|
return "Invalid Nostr npub: incorrect length"
|
|
|
|
return None
|
|
|
|
|
|
def validate_profile_fields(
|
|
contact_email: str | None = None,
|
|
telegram: str | None = None,
|
|
signal: str | None = None,
|
|
nostr_npub: str | None = None,
|
|
) -> dict[str, str]:
|
|
"""
|
|
Validate all profile fields at once.
|
|
|
|
Returns a dict of field_name -> error_message for any invalid fields.
|
|
Empty dict means all fields are valid.
|
|
"""
|
|
errors: dict[str, str] = {}
|
|
|
|
if err := validate_contact_email(contact_email):
|
|
errors["contact_email"] = err
|
|
|
|
if err := validate_telegram(telegram):
|
|
errors["telegram"] = err
|
|
|
|
if err := validate_signal(signal):
|
|
errors["signal"] = err
|
|
|
|
if err := validate_nostr_npub(nostr_npub):
|
|
errors["nostr_npub"] = err
|
|
|
|
return errors
|
|
|