import hashlib import json from dataclasses import dataclass from datetime import UTC, datetime, timedelta from anydi import singleton from cachelib import BaseCache as Cache from fansifter_common.encrypter import Encrypter from fansifter_common.identifiers.exceptions import InvalidDoubleOptInTokenError from fansifter_common.identifiers.types import DoubleOptInToken from fansifter_common.identifiers.utils import decrypt_double_opt_in_identifier from preference_center.adapters.kafka import KafkaProducer from preference_center.config import Settings from preference_center.double_opt_in.exceptions import ( DoubleOptInTokenCheckError, DoubleOptInTokenExpiredError, ) @dataclass(kw_only=True) class DoubleOptInConfirmRequest: token: str @singleton class DoubleOptInConfirmHandler: cache_prefix: str = "double_opt_in_confirm" cache_timeout: int = 86400 token_max_age: timedelta = timedelta(days=30) def __init__( self, encrypter: Encrypter, kafka_producer: KafkaProducer, cache: Cache, settings: Settings, ) -> None: self.encrypter = encrypter self.kafka_producer = kafka_producer self.cache = cache self.settings = settings def handle(self, request: DoubleOptInConfirmRequest) -> None: try: identifier = decrypt_double_opt_in_identifier( self.encrypter, token=DoubleOptInToken(request.token) ) except InvalidDoubleOptInTokenError as exc: raise DoubleOptInTokenCheckError from exc key_hash = hashlib.sha256(identifier.model_dump_json().encode()).hexdigest() if self.cache.get(key=f"{self.cache_prefix}:{key_hash}"): return issued_at = datetime.fromtimestamp(identifier.iat, tz=UTC) if datetime.now(UTC) - issued_at > self.token_max_age: raise DoubleOptInTokenExpiredError self.kafka_producer.publish( topic=self.settings.kafka_topic_double_opt_in, value=json.dumps( { "event_row_id": identifier.event_row_id, "email_id": identifier.email_id, "automated_email_trigger_id": identifier.automated_email_trigger_id, "timestamp": datetime.now(UTC).isoformat(), }, ), key=key_hash, ) self.cache.add( key=f"{self.cache_prefix}:{key_hash}", value=key_hash, timeout=self.cache_timeout, )