"""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:] if not handle: 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