implemented

This commit is contained in:
counterweight 2025-12-20 23:06:05 +01:00
parent a31bd8246c
commit d3638e2e69
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
18 changed files with 1643 additions and 120 deletions

View file

@ -1,7 +1,19 @@
"""Validation utilities for user profile fields."""
import json
from pathlib import Path
from email_validator import validate_email, EmailNotValidError
from bech32 import bech32_decode
# Load validation rules from shared constants
_constants_path = Path(__file__).parent.parent / "shared" / "constants.json"
with open(_constants_path) as f:
_constants = json.load(f)
TELEGRAM_RULES = _constants["validation"]["telegram"]
SIGNAL_RULES = _constants["validation"]["signal"]
NPUB_RULES = _constants["validation"]["nostrNpub"]
def validate_contact_email(value: str | None) -> str | None:
"""
@ -24,22 +36,25 @@ def validate_telegram(value: str | None) -> str | None:
"""
Validate Telegram handle.
Must start with @ if provided, with 1-32 characters after @.
Must start with @ if provided, with characters after @ within max length.
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 @"
prefix = TELEGRAM_RULES["mustStartWith"]
max_len = TELEGRAM_RULES["maxLengthAfterAt"]
if not value.startswith(prefix):
return f"Telegram handle must start with {prefix}"
handle = value[1:]
if not handle:
return "Telegram handle must have at least one character after @"
return f"Telegram handle must have at least one character after {prefix}"
if len(handle) > 32:
return "Telegram handle must be at most 32 characters (after @)"
if len(handle) > max_len:
return f"Telegram handle must be at most {max_len} characters (after {prefix})"
return None
@ -48,19 +63,21 @@ def validate_signal(value: str | None) -> str | None:
"""
Validate Signal username.
Any non-empty string is valid.
Any non-empty string within max length is valid.
Returns None if valid, error message if invalid.
Empty/None values are valid (field is optional).
"""
if not value:
return None
max_len = SIGNAL_RULES["maxLength"]
# 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"
if len(value) > max_len:
return f"Signal username must be at most {max_len} characters"
return None
@ -76,8 +93,11 @@ def validate_nostr_npub(value: str | None) -> str | None:
if not value:
return None
if not value.startswith("npub1"):
return "Nostr npub must start with 'npub1'"
prefix = NPUB_RULES["prefix"]
expected_words = NPUB_RULES["bech32Words"]
if not value.startswith(prefix):
return f"Nostr npub must start with '{prefix}'"
# Decode bech32 to validate checksum
hrp, data = bech32_decode(value)
@ -90,7 +110,7 @@ def validate_nostr_npub(value: str | None) -> str | None:
# 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:
if len(data) != expected_words:
return "Invalid Nostr npub: incorrect length"
return None