mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-10-04 23:12:38 +02:00
990 lines
41 KiB
Python
990 lines
41 KiB
Python
# streaming_providers/providers/magenta2/token_flow_manager.py
|
|
"""
|
|
Hierarchical Token Flow Manager for Magenta2
|
|
Manages the complete token acquisition hierarchy:
|
|
yo_digital → taa → tvhubs → line_auth/remote_login
|
|
"""
|
|
|
|
import time
|
|
from dataclasses import dataclass
|
|
from typing import Any, Dict, Optional
|
|
|
|
from ...base.auth.session_manager import SessionManager
|
|
from ...base.utils.logger import logger
|
|
from .constants import MAGENTA2_FALLBACK_ACCOUNT_URI
|
|
from .sam3_client import Sam3Client
|
|
from .taa_client import TaaClient
|
|
|
|
|
|
@dataclass
|
|
class TokenFlowResult:
|
|
"""Result of token flow operation"""
|
|
|
|
success: bool
|
|
access_token: Optional[str] = None
|
|
refresh_token: Optional[str] = None
|
|
access_token_expires_in: Optional[int] = None
|
|
refresh_token_expires_in: Optional[int] = None
|
|
error: Optional[str] = None
|
|
flow_path: Optional[str] = None # For debugging which path was taken
|
|
|
|
|
|
@dataclass
|
|
class PersonaResult:
|
|
success: bool
|
|
persona_token: Optional[str] = None
|
|
expires_at: Optional[float] = None
|
|
error: Optional[str] = None
|
|
|
|
|
|
class TokenFlowManager:
|
|
"""
|
|
Manages hierarchical token acquisition for Magenta2
|
|
|
|
Token Hierarchy:
|
|
1. yo_digital tokens (goal) - access + refresh, separate expiry
|
|
2. taa access_token (from SAM3 via refresh_token exchange)
|
|
3. tvhubs access_token + shared refresh_token (from line_auth/remote_login)
|
|
|
|
Flow Priority:
|
|
1. Check yo_digital access_token (valid?) → use it ✓
|
|
2. Check yo_digital refresh_token (valid?) → refresh yo_digital
|
|
3. Check taa access_token (valid?) → get yo_digital
|
|
4. Check shared refresh_token (exists?) → exchange for taa → get yo_digital
|
|
5. Try line_auth → get tvhubs + refresh_token → chain to yo_digital
|
|
6. Try remote_login → get tvhubs + refresh_token → chain to yo_digital
|
|
7. All failed → ERROR
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
session_manager: SessionManager,
|
|
sam3_client: "Sam3Client",
|
|
taa_client: "TaaClient",
|
|
provider_name: str,
|
|
country: Optional[str] = None,
|
|
provider_config: Optional[Any] = None,
|
|
# NEW: Add optional callbacks
|
|
line_auth_callback: Optional[callable] = None,
|
|
remote_login_callback: Optional[callable] = None,
|
|
):
|
|
"""
|
|
Initialize TokenFlowManager
|
|
|
|
Args:
|
|
session_manager: SessionManager for token storage
|
|
sam3_client: SAM3 client for tvhubs/taa operations
|
|
taa_client: TAA client for yo_digital operations
|
|
provider_name: Provider name (e.g., 'magenta2')
|
|
country: Optional country code
|
|
"""
|
|
self.session_manager = session_manager
|
|
self.sam3_client = sam3_client
|
|
self.taa_client = taa_client
|
|
self.provider_name = provider_name
|
|
self.country = country
|
|
self.provider_config = provider_config
|
|
|
|
# Store callbacks
|
|
self._line_auth_callback = line_auth_callback
|
|
self._remote_login_callback = remote_login_callback
|
|
|
|
logger.debug(
|
|
f"TokenFlowManager initialized for {provider_name}"
|
|
+ (f" ({country})" if country else "")
|
|
)
|
|
|
|
def get_persona_token(self, force_refresh: bool = False) -> PersonaResult:
|
|
"""Get persona token with proper JWT expiry caching"""
|
|
logger.debug("=== GET_PERSONA_TOKEN START ===")
|
|
|
|
# Check for cached persona token with proper expiry validation
|
|
if not force_refresh:
|
|
cached_result = self._get_cached_persona_token()
|
|
if cached_result.success:
|
|
logger.debug("=== GET_PERSONA_TOKEN SUCCESS (cached) ===")
|
|
return cached_result
|
|
|
|
# Get the yo_digital token
|
|
token_result = self.get_yo_digital_token(force_refresh)
|
|
|
|
if not token_result.success or not token_result.access_token:
|
|
logger.debug("=== GET_PERSONA_TOKEN FAILED (token_result failed) ===")
|
|
return PersonaResult(success=False, error=token_result.error or "No access token")
|
|
|
|
# Compose persona token with expiry information using existing method
|
|
from .token_utils import PersonaTokenComposer
|
|
|
|
composition_result = PersonaTokenComposer.compose_from_jwt(
|
|
token_result.access_token, MAGENTA2_FALLBACK_ACCOUNT_URI
|
|
)
|
|
|
|
if not composition_result:
|
|
logger.debug("=== GET_PERSONA_TOKEN FAILED (composition failed) ===")
|
|
return PersonaResult(success=False, error="Failed to compose persona token")
|
|
|
|
# Cache with the correct expiry (from persona JWT)
|
|
self._cache_persona_composition(composition_result)
|
|
|
|
logger.debug("=== GET_PERSONA_TOKEN SUCCESS ===")
|
|
return PersonaResult(
|
|
success=True,
|
|
persona_token=composition_result.persona_token,
|
|
expires_at=composition_result.expires_at, # 🆕 Return expiry
|
|
)
|
|
|
|
def _get_cached_persona_token(self) -> PersonaResult:
|
|
"""Check for cached persona token using the actual persona JWT expiry"""
|
|
try:
|
|
logger.debug("🟡 Checking cached persona token...")
|
|
persona_data = self.session_manager.load_scoped_token(
|
|
self.provider_name, "persona", self.country
|
|
)
|
|
|
|
safe_persona_data = {
|
|
"has_persona_token": "persona_token" in persona_data,
|
|
"has_persona_jwt": "persona_jwt" in persona_data,
|
|
"expires_at": persona_data.get("expires_at"),
|
|
"composed_at": persona_data.get("composed_at"),
|
|
}
|
|
logger.debug(f"🟡 Loaded persona_data: {safe_persona_data}")
|
|
|
|
if (
|
|
persona_data
|
|
and "persona_token" in persona_data
|
|
and "persona_jwt" in persona_data
|
|
and "expires_at" in persona_data
|
|
):
|
|
|
|
current_time = time.time()
|
|
expires_at = persona_data["expires_at"]
|
|
|
|
logger.debug(f"🟡 Current time: {current_time}, Expires at: {expires_at}")
|
|
|
|
# Check if cached token is still valid using the actual persona JWT expiry
|
|
if current_time < (expires_at - 300): # 5-minute buffer
|
|
logger.debug(
|
|
f"🟡 Using cached persona token (expires at {time.ctime(expires_at)})"
|
|
)
|
|
return PersonaResult(
|
|
success=True,
|
|
persona_token=persona_data["persona_token"],
|
|
expires_at=expires_at, # 🆕 Return expiry
|
|
)
|
|
else:
|
|
logger.debug(f"🟡 Cached persona token expired at {time.ctime(expires_at)}")
|
|
|
|
else:
|
|
logger.debug("🟡 No valid persona data in cache")
|
|
|
|
except Exception as e:
|
|
logger.debug(f"🟡 Error checking cached persona token: {e}")
|
|
|
|
return PersonaResult(success=False, error="No valid cached token")
|
|
|
|
def _cache_persona_composition(self, composition_result) -> None:
|
|
"""Cache persona composition with proper expiry"""
|
|
persona_data = {
|
|
"persona_token": composition_result.persona_token,
|
|
"persona_jwt": composition_result.persona_jwt, # Store for validation
|
|
"expires_at": composition_result.expires_at,
|
|
"composed_at": composition_result.composed_at,
|
|
}
|
|
|
|
success = self.session_manager.save_scoped_token(
|
|
self.provider_name, "persona", persona_data, self.country
|
|
)
|
|
|
|
if success:
|
|
logger.debug(
|
|
f"✓ Persona token cached until {time.ctime(composition_result.expires_at)}"
|
|
)
|
|
else:
|
|
logger.debug("✗ Failed to cache persona token")
|
|
|
|
# Keep the existing _compose_persona_token method for backward compatibility
|
|
@staticmethod
|
|
def _compose_persona_token(access_token: str) -> Optional[str]:
|
|
"""Backward compatibility method - delegates to new composition"""
|
|
from .token_utils import PersonaTokenComposer
|
|
|
|
result = PersonaTokenComposer.compose_from_jwt(access_token, MAGENTA2_FALLBACK_ACCOUNT_URI)
|
|
return result.persona_token if result else None
|
|
|
|
def get_yo_digital_token(self, force_refresh: bool = False) -> TokenFlowResult:
|
|
"""
|
|
Get yo_digital access token following the complete hierarchy
|
|
|
|
Args:
|
|
force_refresh: Skip cached tokens and force refresh
|
|
|
|
Returns:
|
|
TokenFlowResult with yo_digital access token or error
|
|
"""
|
|
country_str = f" ({self.country})" if self.country else ""
|
|
logger.info(f"Getting yo_digital token for {self.provider_name}{country_str}")
|
|
|
|
if not force_refresh:
|
|
# Step 1: Check existing yo_digital access_token
|
|
result = self._check_yo_digital_access_token()
|
|
if result.success:
|
|
logger.info("✓ Using valid yo_digital access_token")
|
|
return result
|
|
|
|
# Step 2: Check yo_digital refresh_token
|
|
result = self._refresh_yo_digital_if_possible()
|
|
if result.success:
|
|
logger.info("✓ Refreshed yo_digital tokens")
|
|
return result
|
|
|
|
# Step 3: Check taa access_token
|
|
result = self._get_yo_digital_from_taa()
|
|
if result.success:
|
|
logger.info("✓ Got yo_digital tokens from taa")
|
|
return result
|
|
|
|
# Step 4: Check shared refresh_token
|
|
result = self._get_yo_digital_via_taa_exchange()
|
|
if result.success:
|
|
logger.info("✓ Got yo_digital tokens via taa exchange")
|
|
return result
|
|
|
|
# Step 5: Try line_auth
|
|
result = self._get_yo_digital_via_line_auth()
|
|
if result.success:
|
|
logger.info("✓ Got yo_digital tokens via line_auth")
|
|
return result
|
|
|
|
# Step 6: Try remote_login
|
|
result = self._get_yo_digital_via_remote_login()
|
|
if result.success:
|
|
logger.info("✓ Got yo_digital tokens via remote_login")
|
|
return result
|
|
|
|
# All failed
|
|
logger.error("✗ All token acquisition methods failed")
|
|
return TokenFlowResult(
|
|
success=False,
|
|
error="All token acquisition methods failed",
|
|
flow_path="all_failed",
|
|
)
|
|
|
|
# ========================================================================
|
|
# Step 1: Check existing yo_digital access_token
|
|
# ========================================================================
|
|
|
|
def _check_yo_digital_access_token(self) -> TokenFlowResult:
|
|
"""Check if we have a valid yo_digital access_token"""
|
|
try:
|
|
token_data = self.session_manager.load_scoped_token(
|
|
self.provider_name, "yo_digital", self.country
|
|
)
|
|
|
|
if not token_data or "access_token" not in token_data:
|
|
return TokenFlowResult(
|
|
success=False,
|
|
error="No yo_digital token found",
|
|
flow_path="check_yo_digital_access",
|
|
)
|
|
|
|
# Check if access_token is still valid
|
|
if self._is_yo_digital_access_token_valid(token_data):
|
|
return TokenFlowResult(
|
|
success=True,
|
|
access_token=token_data["access_token"],
|
|
refresh_token=token_data.get("refresh_token"),
|
|
flow_path="yo_digital_access_valid",
|
|
)
|
|
|
|
return TokenFlowResult(
|
|
success=False,
|
|
error="yo_digital access_token expired",
|
|
flow_path="check_yo_digital_access",
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.debug(f"Error checking yo_digital access_token: {e}")
|
|
return TokenFlowResult(success=False, error=str(e), flow_path="check_yo_digital_access")
|
|
|
|
# ========================================================================
|
|
# Step 2: Refresh yo_digital tokens
|
|
# ========================================================================
|
|
|
|
def _refresh_yo_digital_if_possible(self) -> TokenFlowResult:
|
|
"""Try to refresh yo_digital tokens if refresh_token is valid"""
|
|
try:
|
|
token_data = self.session_manager.load_scoped_token(
|
|
self.provider_name, "yo_digital", self.country
|
|
)
|
|
|
|
if not token_data or "refresh_token" not in token_data:
|
|
return TokenFlowResult(
|
|
success=False,
|
|
error="No yo_digital refresh_token found",
|
|
flow_path="refresh_yo_digital",
|
|
)
|
|
|
|
# Check if refresh_token is still valid
|
|
if not self._is_yo_digital_refresh_token_valid(token_data):
|
|
return TokenFlowResult(
|
|
success=False,
|
|
error="yo_digital refresh_token expired",
|
|
flow_path="refresh_yo_digital",
|
|
)
|
|
|
|
# Refresh via TaaClient (stub for now)
|
|
logger.debug("Attempting to refresh yo_digital tokens")
|
|
new_tokens_dict = self.taa_client.refresh_yo_digital_tokens(token_data["refresh_token"])
|
|
|
|
if not new_tokens_dict:
|
|
return TokenFlowResult(
|
|
success=False,
|
|
error="yo_digital refresh failed",
|
|
flow_path="refresh_yo_digital",
|
|
)
|
|
|
|
# Save new tokens (already in dict format from stub)
|
|
self._save_yo_digital_tokens(new_tokens_dict)
|
|
|
|
return TokenFlowResult(
|
|
success=True,
|
|
access_token=new_tokens_dict.get("accessToken")
|
|
or new_tokens_dict.get("access_token"),
|
|
refresh_token=new_tokens_dict.get("refreshToken")
|
|
or new_tokens_dict.get("refresh_token"),
|
|
flow_path="yo_digital_refreshed",
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.debug(f"Error refreshing yo_digital tokens: {e}")
|
|
return TokenFlowResult(success=False, error=str(e), flow_path="refresh_yo_digital")
|
|
|
|
# ========================================================================
|
|
# Step 3: Get yo_digital from taa access_token
|
|
# ========================================================================
|
|
|
|
def _get_yo_digital_from_taa(self) -> TokenFlowResult:
|
|
"""Get yo_digital tokens using existing taa access_token"""
|
|
try:
|
|
# Check if we have valid taa access_token
|
|
taa_token_data = self.session_manager.load_scoped_token(
|
|
self.provider_name, "taa", self.country
|
|
)
|
|
|
|
if not taa_token_data or "access_token" not in taa_token_data:
|
|
return TokenFlowResult(
|
|
success=False,
|
|
error="No taa access_token found",
|
|
flow_path="yo_digital_from_taa",
|
|
)
|
|
|
|
# Check if taa token is still valid
|
|
if self._is_token_expired(taa_token_data):
|
|
return TokenFlowResult(
|
|
success=False,
|
|
error="taa access_token expired",
|
|
flow_path="yo_digital_from_taa",
|
|
)
|
|
|
|
# Get yo_digital tokens from TAA endpoint
|
|
logger.debug("Getting yo_digital tokens from taa access_token")
|
|
yo_digital_result = self.taa_client.get_yo_digital_tokens(
|
|
taa_access_token=taa_token_data["access_token"],
|
|
device_id=self.session_manager.get_device_id(self.provider_name, self.country),
|
|
)
|
|
|
|
if not yo_digital_result:
|
|
# EDGE CASE: TAA token exists but yo_digital failed
|
|
# This happens when cached TAA is from ISP account, not TV account
|
|
logger.warning(
|
|
"yo_digital acquisition failed with valid taa token - "
|
|
"likely cached TAA from ISP account, TV subscription on different account"
|
|
)
|
|
logger.info("Clearing cached tokens and falling back to remote_login")
|
|
|
|
# Clear the invalid cached tokens to prevent repeated failures
|
|
self.session_manager.clear_scoped_token(self.provider_name, "taa", self.country)
|
|
self.session_manager.clear_scoped_token(self.provider_name, "tvhubs", self.country)
|
|
|
|
# Fallback to remote_login
|
|
return self._get_yo_digital_via_remote_login()
|
|
|
|
# Convert YoDigitalTokens to dict for saving
|
|
yo_digital_dict = {
|
|
"accessToken": yo_digital_result.access_token,
|
|
"accessExpiresIn": yo_digital_result.access_token_expires_in,
|
|
"refreshToken": yo_digital_result.refresh_token,
|
|
"refreshExpiresIn": yo_digital_result.refresh_token_expires_in,
|
|
}
|
|
|
|
# Save yo_digital tokens
|
|
self._save_yo_digital_tokens(yo_digital_dict)
|
|
|
|
return TokenFlowResult(
|
|
success=True,
|
|
access_token=yo_digital_result.access_token,
|
|
refresh_token=yo_digital_result.refresh_token,
|
|
flow_path="yo_digital_from_taa",
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.debug(f"Error getting yo_digital from taa: {e}")
|
|
return TokenFlowResult(success=False, error=str(e), flow_path="yo_digital_from_taa")
|
|
|
|
# ========================================================================
|
|
# Step 4: Exchange shared refresh_token for taa, then yo_digital
|
|
# ========================================================================
|
|
|
|
def _get_yo_digital_via_taa_exchange(self) -> TokenFlowResult:
|
|
"""Exchange shared refresh_token for taa, then get yo_digital"""
|
|
try:
|
|
# Check if we have shared refresh_token at provider level
|
|
session_data = self.session_manager.load_session(self.provider_name, self.country)
|
|
|
|
if not session_data or "refresh_token" not in session_data:
|
|
return TokenFlowResult(
|
|
success=False,
|
|
error="No shared refresh_token found",
|
|
flow_path="yo_digital_via_taa_exchange",
|
|
)
|
|
|
|
# Exchange refresh_token for taa access_token
|
|
logger.debug("Exchanging refresh_token for taa access_token")
|
|
taa_token = self.sam3_client.get_token(
|
|
grant_type="refresh_token",
|
|
scope="taa",
|
|
credential1=session_data["refresh_token"],
|
|
)
|
|
|
|
if not taa_token:
|
|
return TokenFlowResult(
|
|
success=False,
|
|
error="Failed to exchange refresh_token for taa",
|
|
flow_path="yo_digital_via_taa_exchange",
|
|
)
|
|
|
|
# Save taa token
|
|
self._save_taa_token(taa_token)
|
|
|
|
if self.sam3_client.refresh_token:
|
|
logger.debug("Updating session-level refresh_token after taa exchange")
|
|
self._save_refresh_token(self.sam3_client.refresh_token)
|
|
|
|
# Now get yo_digital tokens using taa
|
|
logger.debug("Getting yo_digital tokens from exchanged taa token")
|
|
yo_digital_result = self.taa_client.get_yo_digital_tokens(
|
|
taa_access_token=taa_token,
|
|
device_id=self.session_manager.get_device_id(self.provider_name, self.country),
|
|
)
|
|
|
|
if not yo_digital_result:
|
|
# EDGE CASE: Refresh token exchange succeeded but yo_digital failed
|
|
# This happens when shared refresh_token is from ISP account, not TV account
|
|
logger.warning(
|
|
"yo_digital acquisition failed after successful taa exchange - "
|
|
"likely shared refresh_token from ISP account, TV subscription on different account"
|
|
)
|
|
logger.info("Clearing cached tokens and falling back to remote_login")
|
|
|
|
# Clear the invalid cached tokens and refresh_token
|
|
self.session_manager.clear_scoped_token(self.provider_name, "taa", self.country)
|
|
self.session_manager.clear_scoped_token(self.provider_name, "tvhubs", self.country)
|
|
|
|
# Clear the shared refresh_token from session data
|
|
session_data = self.session_manager.load_session(self.provider_name, self.country)
|
|
if session_data and "refresh_token" in session_data:
|
|
del session_data["refresh_token"]
|
|
self.session_manager.save_session(
|
|
self.provider_name, session_data, self.country
|
|
)
|
|
|
|
# Fallback to remote_login
|
|
return self._get_yo_digital_via_remote_login()
|
|
|
|
# Convert YoDigitalTokens to dict for saving
|
|
yo_digital_dict = {
|
|
"accessToken": yo_digital_result.access_token,
|
|
"accessExpiresIn": yo_digital_result.access_token_expires_in,
|
|
"refreshToken": yo_digital_result.refresh_token,
|
|
"refreshExpiresIn": yo_digital_result.refresh_token_expires_in,
|
|
}
|
|
|
|
# Save yo_digital tokens
|
|
self._save_yo_digital_tokens(yo_digital_dict)
|
|
|
|
return TokenFlowResult(
|
|
success=True,
|
|
access_token=yo_digital_result.access_token,
|
|
refresh_token=yo_digital_result.refresh_token,
|
|
flow_path="yo_digital_via_taa_exchange",
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.debug(f"Error in taa exchange flow: {e}")
|
|
return TokenFlowResult(
|
|
success=False, error=str(e), flow_path="yo_digital_via_taa_exchange"
|
|
)
|
|
|
|
# ========================================================================
|
|
# Step 5: Line auth → tvhubs + refresh → taa → yo_digital
|
|
# ========================================================================
|
|
|
|
def _get_yo_digital_via_line_auth(self) -> TokenFlowResult:
|
|
"""Try line_auth to get tvhubs + refresh_token, then chain to yo_digital"""
|
|
try:
|
|
# Check if callback is available
|
|
if not self._line_auth_callback:
|
|
return TokenFlowResult(
|
|
success=False,
|
|
error="line_auth callback not configured",
|
|
flow_path="yo_digital_via_line_auth",
|
|
)
|
|
|
|
logger.info("Attempting line_auth flow via callback")
|
|
|
|
# Call the authenticator's line auth method
|
|
line_response = self._line_auth_callback()
|
|
|
|
if not line_response:
|
|
return TokenFlowResult(
|
|
success=False,
|
|
error="line_auth failed",
|
|
flow_path="yo_digital_via_line_auth",
|
|
)
|
|
|
|
# Save tvhubs token
|
|
self._save_tvhubs_token(line_response)
|
|
|
|
# Save refresh token at provider level
|
|
if "refresh_token" in line_response:
|
|
self._save_refresh_token(line_response["refresh_token"])
|
|
|
|
# Now exchange for taa
|
|
logger.debug("Exchanging line_auth refresh_token for taa")
|
|
taa_token = self.sam3_client.get_token(
|
|
grant_type="refresh_token",
|
|
scope="taa",
|
|
credential1=line_response["refresh_token"],
|
|
)
|
|
|
|
if not taa_token:
|
|
return TokenFlowResult(
|
|
success=False,
|
|
error="Failed to exchange line_auth refresh for taa",
|
|
flow_path="yo_digital_via_line_auth",
|
|
)
|
|
|
|
# Save taa token
|
|
self._save_taa_token(taa_token)
|
|
|
|
if self.sam3_client.refresh_token:
|
|
logger.debug("Updating session-level refresh_token after line_auth taa exchange")
|
|
self._save_refresh_token(self.sam3_client.refresh_token)
|
|
|
|
# Finally get yo_digital
|
|
logger.debug("Getting yo_digital tokens from line_auth taa token")
|
|
yo_digital_result = self.taa_client.get_yo_digital_tokens(
|
|
taa_access_token=taa_token,
|
|
device_id=self.session_manager.get_device_id(self.provider_name, self.country),
|
|
)
|
|
|
|
if not yo_digital_result:
|
|
# EDGE CASE: Line auth succeeded but yo_digital failed
|
|
# This happens when ISP account != TV account
|
|
logger.warning(
|
|
"yo_digital acquisition failed after successful line_auth - "
|
|
"likely ISP account differs from TV subscription account"
|
|
)
|
|
logger.info("Falling back to remote_login for TV account authentication")
|
|
|
|
# Fallback to remote_login (tokens will be overwritten)
|
|
return self._get_yo_digital_via_remote_login()
|
|
|
|
# Convert YoDigitalTokens to dict for saving
|
|
yo_digital_dict = {
|
|
"accessToken": yo_digital_result.access_token,
|
|
"accessExpiresIn": yo_digital_result.access_token_expires_in,
|
|
"refreshToken": yo_digital_result.refresh_token,
|
|
"refreshExpiresIn": yo_digital_result.refresh_token_expires_in,
|
|
}
|
|
|
|
# Save yo_digital tokens
|
|
self._save_yo_digital_tokens(yo_digital_dict)
|
|
|
|
return TokenFlowResult(
|
|
success=True,
|
|
access_token=yo_digital_result.access_token,
|
|
refresh_token=yo_digital_result.refresh_token,
|
|
flow_path="yo_digital_via_line_auth",
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.debug(f"Error in line_auth flow: {e}")
|
|
return TokenFlowResult(
|
|
success=False, error=str(e), flow_path="yo_digital_via_line_auth"
|
|
)
|
|
|
|
# ========================================================================
|
|
# Step 6: Remote login → tvhubs + refresh → taa → yo_digital
|
|
# ========================================================================
|
|
|
|
def _get_yo_digital_via_remote_login(self) -> TokenFlowResult:
|
|
"""Try remote_login to get tvhubs + refresh_token, then chain to yo_digital"""
|
|
try:
|
|
if self._remote_login_callback:
|
|
logger.info("Attempting remote_login flow via callback")
|
|
remote_token_data = self._remote_login_callback()
|
|
elif self.sam3_client.can_use_remote_login():
|
|
logger.info("Attempting remote_login flow via sam3_client (no callback)")
|
|
remote_token_data = self.sam3_client.remote_login(scope="tvhubs offline_access")
|
|
else:
|
|
return TokenFlowResult(
|
|
success=False,
|
|
error="remote_login not configured",
|
|
flow_path="yo_digital_via_remote_login",
|
|
)
|
|
|
|
if not remote_token_data:
|
|
return TokenFlowResult(
|
|
success=False,
|
|
error="remote_login failed or timed out",
|
|
flow_path="yo_digital_via_remote_login",
|
|
)
|
|
|
|
# Save tvhubs token
|
|
self._save_tvhubs_token(remote_token_data)
|
|
|
|
# Save refresh token at provider level
|
|
if "refresh_token" in remote_token_data:
|
|
self._save_refresh_token(remote_token_data["refresh_token"])
|
|
|
|
# Now exchange for taa
|
|
logger.debug("Exchanging remote_login refresh_token for taa")
|
|
taa_token = self.sam3_client.get_token(
|
|
grant_type="refresh_token",
|
|
scope="taa",
|
|
credential1=remote_token_data["refresh_token"],
|
|
)
|
|
|
|
if not taa_token:
|
|
return TokenFlowResult(
|
|
success=False,
|
|
error="Failed to exchange remote_login refresh for taa",
|
|
flow_path="yo_digital_via_remote_login",
|
|
)
|
|
|
|
# Save taa token
|
|
self._save_taa_token(taa_token)
|
|
|
|
if self.sam3_client.refresh_token:
|
|
logger.debug("Updating session-level refresh_token after remote_login taa exchange")
|
|
self._save_refresh_token(self.sam3_client.refresh_token)
|
|
|
|
# Finally get yo_digital
|
|
logger.debug("Getting yo_digital tokens from remote_login taa token")
|
|
yo_digital_result = self.taa_client.get_yo_digital_tokens(
|
|
taa_access_token=taa_token,
|
|
device_id=self.session_manager.get_device_id(self.provider_name, self.country),
|
|
)
|
|
|
|
if not yo_digital_result:
|
|
return TokenFlowResult(
|
|
success=False,
|
|
error="Failed to get yo_digital from remote_login taa",
|
|
flow_path="yo_digital_via_remote_login",
|
|
)
|
|
|
|
# Convert YoDigitalTokens to dict for saving
|
|
yo_digital_dict = {
|
|
"accessToken": yo_digital_result.access_token,
|
|
"accessExpiresIn": yo_digital_result.access_token_expires_in,
|
|
"refreshToken": yo_digital_result.refresh_token,
|
|
"refreshExpiresIn": yo_digital_result.refresh_token_expires_in,
|
|
}
|
|
|
|
# Save yo_digital tokens
|
|
self._save_yo_digital_tokens(yo_digital_dict)
|
|
|
|
return TokenFlowResult(
|
|
success=True,
|
|
access_token=yo_digital_result.access_token,
|
|
refresh_token=yo_digital_result.refresh_token,
|
|
flow_path="yo_digital_via_remote_login",
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.debug(f"Error in remote_login flow: {e}")
|
|
return TokenFlowResult(
|
|
success=False, error=str(e), flow_path="yo_digital_via_remote_login"
|
|
)
|
|
|
|
# ========================================================================
|
|
# Helper Methods - Token Validation
|
|
# ========================================================================
|
|
|
|
@staticmethod
|
|
def _is_yo_digital_access_token_valid(token_data: Dict[str, Any]) -> bool:
|
|
"""Check if yo_digital access_token is still valid"""
|
|
if (
|
|
"access_token_expires_in" not in token_data
|
|
or "access_token_issued_at" not in token_data
|
|
):
|
|
return False
|
|
|
|
expires_at = token_data["access_token_issued_at"] + token_data["access_token_expires_in"]
|
|
# Use 5 minute buffer
|
|
return time.time() < (expires_at - 300)
|
|
|
|
@staticmethod
|
|
def _is_yo_digital_refresh_token_valid(token_data: Dict[str, Any]) -> bool:
|
|
"""Check if yo_digital refresh_token is still valid"""
|
|
if (
|
|
"refresh_token_expires_in" not in token_data
|
|
or "refresh_token_issued_at" not in token_data
|
|
):
|
|
return False
|
|
|
|
expires_at = token_data["refresh_token_issued_at"] + token_data["refresh_token_expires_in"]
|
|
# Use 5 minute buffer
|
|
return time.time() < (expires_at - 300)
|
|
|
|
@staticmethod
|
|
def _is_token_expired(token_data: Dict[str, Any]) -> bool:
|
|
"""Check if standard token (tvhubs/taa) is expired"""
|
|
if "expires_in" not in token_data or "issued_at" not in token_data:
|
|
return True
|
|
|
|
expires_at = token_data["issued_at"] + token_data["expires_in"]
|
|
# Use 5 minute buffer
|
|
return time.time() >= (expires_at - 300)
|
|
|
|
# ========================================================================
|
|
# Helper Methods - Token Storage
|
|
# ========================================================================
|
|
|
|
def _save_yo_digital_tokens(self, tokens: Dict[str, Any]) -> None:
|
|
"""Save yo_digital tokens with proper format"""
|
|
current_time = time.time()
|
|
|
|
token_data = {
|
|
"access_token": tokens.get("accessToken") or tokens.get("access_token"),
|
|
"access_token_expires_in": tokens.get("accessExpiresIn")
|
|
or tokens.get("access_token_expires_in", 86400),
|
|
"access_token_issued_at": current_time,
|
|
"refresh_token": tokens.get("refreshToken") or tokens.get("refresh_token"),
|
|
"refresh_token_expires_in": tokens.get("refreshExpiresIn")
|
|
or tokens.get("refresh_token_expires_in", 86400),
|
|
"refresh_token_issued_at": current_time,
|
|
"token_type": "Bearer",
|
|
}
|
|
|
|
success = self.session_manager.save_scoped_token(
|
|
self.provider_name, "yo_digital", token_data, self.country
|
|
)
|
|
|
|
if success:
|
|
logger.info("✓ yo_digital tokens saved")
|
|
else:
|
|
logger.error("✗ Failed to save yo_digital tokens")
|
|
|
|
def _save_taa_token(self, taa_token: str) -> None:
|
|
"""Save taa access_token"""
|
|
token_data = {
|
|
"access_token": taa_token,
|
|
"token_type": "Bearer",
|
|
"expires_in": 3600, # Default 1 hour
|
|
"issued_at": time.time(),
|
|
}
|
|
|
|
self.session_manager.save_scoped_token(self.provider_name, "taa", token_data, self.country)
|
|
logger.debug("taa token saved")
|
|
|
|
def _save_tvhubs_token(self, token_data: Dict[str, Any]) -> None:
|
|
"""Save tvhubs access_token"""
|
|
tvhubs_data = {
|
|
"access_token": token_data.get("access_token"),
|
|
"token_type": token_data.get("token_type", "Bearer"),
|
|
"expires_in": token_data.get("expires_in", 7200),
|
|
"issued_at": time.time(),
|
|
}
|
|
|
|
self.session_manager.save_scoped_token(
|
|
self.provider_name, "tvhubs", tvhubs_data, self.country
|
|
)
|
|
logger.debug("tvhubs token saved")
|
|
|
|
# ========================================================================
|
|
# Public API - tvhubs token (centralized lifecycle)
|
|
# ========================================================================
|
|
|
|
def get_tvhubs_token(self) -> Optional[str]:
|
|
"""
|
|
Return a valid tvhubs-scoped access_token, managing the full lifecycle:
|
|
|
|
1. Cached token still valid? → return it
|
|
2. Expired + shared refresh_token? → refresh tvhubs, save both
|
|
access_token and refresh_token
|
|
3. Refresh token dead/missing? → clear stale state, fall through
|
|
to the full re-auth chain
|
|
(line_auth → remote_login)
|
|
4. Full chain succeeded? → tvhubs token now in session,
|
|
return it
|
|
|
|
This is the single place that owns tvhubs token lifecycle at runtime.
|
|
provider.py must never inline refresh/save logic — call this instead.
|
|
"""
|
|
# Step 1: valid cached token
|
|
try:
|
|
token_data = self.session_manager.load_scoped_token(
|
|
self.provider_name, "tvhubs", self.country
|
|
)
|
|
if token_data and token_data.get("access_token"):
|
|
if not self._is_token_expired(token_data):
|
|
logger.debug("tvhubs token: using valid cached token")
|
|
return token_data["access_token"]
|
|
except Exception as exc:
|
|
logger.debug(f"tvhubs token: error reading cache: {exc}")
|
|
|
|
# Step 2: try refreshing via shared refresh_token
|
|
try:
|
|
session_data = self.session_manager.load_session(self.provider_name, self.country) or {}
|
|
shared_rt = self.sam3_client.refresh_token or session_data.get("refresh_token")
|
|
|
|
if shared_rt:
|
|
logger.debug("tvhubs token: expired, attempting refresh")
|
|
self.sam3_client.refresh_token = shared_rt
|
|
new_access_token = self.sam3_client.refresh_access_token("tvhubs")
|
|
|
|
if new_access_token:
|
|
# Persist the new access_token
|
|
self._save_tvhubs_token({
|
|
"access_token": new_access_token,
|
|
"token_type": "Bearer",
|
|
"expires_in": 7200,
|
|
})
|
|
# Persist the new refresh_token — the old one is now invalid
|
|
if self.sam3_client.refresh_token:
|
|
self._save_refresh_token(self.sam3_client.refresh_token)
|
|
logger.debug("tvhubs token: refresh_token updated in session")
|
|
logger.debug("tvhubs token: refreshed successfully")
|
|
return new_access_token
|
|
|
|
# Refresh call failed → the refresh_token is dead; clear it so
|
|
# downstream steps don't waste a round-trip attempting it again.
|
|
logger.warning(
|
|
"tvhubs token: refresh failed — shared refresh_token likely invalidated; "
|
|
"clearing and falling back to full re-auth chain"
|
|
)
|
|
if "refresh_token" in session_data:
|
|
del session_data["refresh_token"]
|
|
self.session_manager.save_session(self.provider_name, session_data, self.country)
|
|
self.sam3_client.refresh_token = None
|
|
else:
|
|
logger.debug("tvhubs token: no shared refresh_token available, skipping refresh")
|
|
|
|
except Exception as exc:
|
|
logger.warning(f"tvhubs token: error during refresh attempt: {exc}")
|
|
|
|
# Step 3/4: fall back to the full re-auth chain (steps 5/6 in
|
|
# get_yo_digital_token: line_auth → remote_login). We call
|
|
# get_yo_digital_token with force_refresh=True so it skips the
|
|
# yo_digital / taa checks (already known stale) and goes straight
|
|
# to the login flows which re-acquire tvhubs + refresh_token as a
|
|
# side effect.
|
|
logger.info("tvhubs token: entering full re-auth chain")
|
|
result = self.get_yo_digital_token(force_refresh=True)
|
|
if result.success:
|
|
# The chain saved a fresh tvhubs token as a side effect — return it.
|
|
try:
|
|
token_data = self.session_manager.load_scoped_token(
|
|
self.provider_name, "tvhubs", self.country
|
|
)
|
|
if token_data and token_data.get("access_token"):
|
|
logger.debug("tvhubs token: obtained via full re-auth chain")
|
|
return token_data["access_token"]
|
|
except Exception as exc:
|
|
logger.debug(f"tvhubs token: error reading token after re-auth: {exc}")
|
|
|
|
logger.warning("tvhubs token: all acquisition attempts failed")
|
|
return None
|
|
|
|
def _save_refresh_token(self, refresh_token: str) -> None:
|
|
"""Save shared refresh_token at provider level"""
|
|
session_data = self.session_manager.load_session(self.provider_name, self.country) or {}
|
|
|
|
session_data["refresh_token"] = refresh_token
|
|
session_data["device_id"] = self.session_manager.get_device_id(
|
|
self.provider_name, self.country
|
|
)
|
|
|
|
self.session_manager.save_session(self.provider_name, session_data, self.country)
|
|
logger.debug("Shared refresh_token saved")
|
|
|
|
# ========================================================================
|
|
# Public API - Debugging
|
|
# ========================================================================
|
|
|
|
def get_token_status(self) -> Dict[str, Any]:
|
|
"""Get comprehensive status of all tokens"""
|
|
status = {
|
|
"yo_digital": self._get_yo_digital_status(),
|
|
"taa": self._get_taa_status(),
|
|
"tvhubs": self._get_tvhubs_status(),
|
|
"refresh_token": self._get_refresh_token_status(),
|
|
}
|
|
|
|
return status
|
|
|
|
def _get_yo_digital_status(self) -> Dict[str, Any]:
|
|
"""Get yo_digital token status"""
|
|
token_data = self.session_manager.load_scoped_token(
|
|
self.provider_name, "yo_digital", self.country
|
|
)
|
|
|
|
if not token_data:
|
|
return {"exists": False}
|
|
|
|
return {
|
|
"exists": True,
|
|
"has_access_token": "access_token" in token_data,
|
|
"access_token_valid": self._is_yo_digital_access_token_valid(token_data),
|
|
"has_refresh_token": "refresh_token" in token_data,
|
|
"refresh_token_valid": self._is_yo_digital_refresh_token_valid(token_data),
|
|
}
|
|
|
|
def _get_taa_status(self) -> Dict[str, Any]:
|
|
"""Get taa token status"""
|
|
token_data = self.session_manager.load_scoped_token(self.provider_name, "taa", self.country)
|
|
|
|
if not token_data:
|
|
return {"exists": False}
|
|
|
|
return {
|
|
"exists": True,
|
|
"has_access_token": "access_token" in token_data,
|
|
"is_valid": not self._is_token_expired(token_data),
|
|
}
|
|
|
|
def _get_tvhubs_status(self) -> Dict[str, Any]:
|
|
"""Get tvhubs token status"""
|
|
token_data = self.session_manager.load_scoped_token(
|
|
self.provider_name, "tvhubs", self.country
|
|
)
|
|
|
|
if not token_data:
|
|
return {"exists": False}
|
|
|
|
return {
|
|
"exists": True,
|
|
"has_access_token": "access_token" in token_data,
|
|
"is_valid": not self._is_token_expired(token_data),
|
|
}
|
|
|
|
def _get_refresh_token_status(self) -> Dict[str, Any]:
|
|
"""Get shared refresh_token status"""
|
|
session_data = self.session_manager.load_session(self.provider_name, self.country)
|
|
|
|
if not session_data or "refresh_token" not in session_data:
|
|
return {"exists": False}
|
|
|
|
return {"exists": True, "has_refresh_token": True} |