from pydantic import BaseModel from yuxi.channel.extensions.twitch.types import TwitchRole class TwitchAccountConfig(BaseModel): username: str = "" access_token: str = "" client_id: str | None = None channel: str = "" enabled: bool = True allow_from: list[str] = [] allowed_roles: list[TwitchRole] = [] require_mention: bool = True response_prefix: str | None = None client_secret: str | None = None refresh_token: str | None = None expires_in: int | None = None obtainment_timestamp: float | None = None class TwitchConfig(BaseModel): enabled: bool = True name: str | None = None default_account: str = "default" username: str | None = None access_token: str | None = None client_id: str | None = None channel: str | None = None require_mention: bool = True allow_from: list[str] = [] allowed_roles: list[TwitchRole] = [] response_prefix: str | None = None client_secret: str | None = None refresh_token: str | None = None accounts: dict[str, TwitchAccountConfig] = {} block_streaming: bool = True block_streaming_coalesce_max_delay_ms: int = 500 block_streaming_coalesce_min_chars: int = 100 send_chunk_interval_ms: int = 1500 eventsub_enabled: bool = True _MERGE_FIELDS = ( "username", "access_token", "client_id", "channel", "require_mention", "allow_from", "allowed_roles", ) def normalize_twitch_channel(channel: str) -> str: return channel.strip().removeprefix("#").lower() def is_account_configured(account: TwitchAccountConfig) -> bool: return bool(account.username and account.access_token and account.channel) def get_account_config(config: TwitchConfig, account_id: str = "default") -> TwitchAccountConfig: account = config.accounts.get(account_id) if account is None: if account_id == "default": return TwitchAccountConfig( username=config.username or "", access_token=config.access_token or "", client_id=config.client_id, channel=config.channel or "", require_mention=config.require_mention, allow_from=list(config.allow_from), allowed_roles=list(config.allowed_roles), response_prefix=config.response_prefix, ) return TwitchAccountConfig() if account_id == "default": merged = account.model_copy() for field_name in _MERGE_FIELDS: base_val = getattr(config, field_name, None) is_empty = False if isinstance(base_val, str) and not base_val: is_empty = True elif isinstance(base_val, list) and not base_val: is_empty = True if base_val is None or is_empty: continue setattr(merged, field_name, base_val) return merged return account def list_account_ids(config: TwitchConfig) -> list[str]: if config.accounts: return list(config.accounts.keys()) return ["default"] def has_configured_state(config: TwitchConfig) -> bool: if config.accounts: return any(is_account_configured(acct) for acct in config.accounts.values()) default = TwitchAccountConfig( username=config.username or "", access_token=config.access_token or "", channel=config.channel or "", ) return is_account_configured(default) def resolve_default_to(config: TwitchConfig, account_id: str | None = None) -> str | None: aid = account_id or config.default_account account = get_account_config(config, aid) if is_account_configured(account): return normalize_twitch_channel(account.channel) return None