Coverage for bot/bootstrap_runtime.py: 71%
99 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-02-25 12:34 -0300
« prev ^ index » next coverage.py v7.10.7, created at 2026-02-25 12:34 -0300
1import asyncio
2import os
4from bot.autonomy_runtime import autonomy_runtime
5from bot.clip_jobs_runtime import clip_jobs
6from bot.eventsub_runtime import ByteBot
7from bot.irc_runtime import IrcByteBot
8from bot.observability import observability
9from bot.runtime_config import (
10 CLIENT_ID,
11 TWITCH_BOT_LOGIN,
12 TWITCH_CHANNEL_LOGIN,
13 TWITCH_CHANNEL_LOGINS_RAW,
14 TWITCH_CLIENT_SECRET_INLINE,
15 TWITCH_CLIENT_SECRET_NAME,
16 TWITCH_IRC_HOST,
17 TWITCH_IRC_PORT,
18 TWITCH_IRC_TLS,
19 TWITCH_REFRESH_TOKEN,
20 TWITCH_TOKEN_REFRESH_MARGIN_SECONDS,
21 TWITCH_TOKEN_REFRESH_TIMEOUT_SECONDS,
22 TWITCH_TOKEN_VALIDATE_TIMEOUT_SECONDS,
23 TWITCH_USER_TOKEN,
24 irc_channel_control,
25 logger,
26)
27from bot.status_runtime import parse_channel_logins
28from bot.twitch_tokens import TwitchTokenManager, TwitchTokenManagerSettings
31def get_secret(secret_name: str = "twitch-client-secret") -> str:
32 # Ler do ENV usando o nome do secret
33 env_key = secret_name.upper().replace("-", "_")
34 val = os.environ.get(env_key)
35 if val:
36 return val
37 raise RuntimeError(f"Secret {env_key} not in env.")
40def require_env(name: str) -> str:
41 value = (os.environ.get(name) or "").strip()
42 if not value:
43 raise RuntimeError(f"Variavel obrigatoria ausente: {name}")
44 return value
47def resolve_irc_channel_logins() -> list[str]:
48 explicit_channels = parse_channel_logins(TWITCH_CHANNEL_LOGINS_RAW)
49 if explicit_channels:
50 return explicit_channels
51 fallback_channels = parse_channel_logins(TWITCH_CHANNEL_LOGIN)
52 if fallback_channels:
53 return fallback_channels
55 required_single_channel = require_env("TWITCH_CHANNEL_LOGIN")
56 required_channels = parse_channel_logins(required_single_channel)
57 if not required_channels:
58 raise RuntimeError(
59 "TWITCH_CHANNEL_LOGIN invalido. Use login Twitch sem # e com 3-25 caracteres."
60 )
61 return required_channels
64def resolve_client_secret_for_irc_refresh() -> str:
65 if TWITCH_CLIENT_SECRET_INLINE:
66 return TWITCH_CLIENT_SECRET_INLINE
68 secret_name = TWITCH_CLIENT_SECRET_NAME or "twitch-client-secret"
69 try:
70 return get_secret(secret_name=secret_name)
71 except Exception as error:
72 logger.warning(
73 "Nao foi possivel ler segredo '%s' para refresh automatico: %s",
74 secret_name,
75 error,
76 )
77 return ""
80def build_irc_token_manager() -> TwitchTokenManager:
81 user_token = TWITCH_USER_TOKEN or require_env("TWITCH_USER_TOKEN")
82 refresh_token = TWITCH_REFRESH_TOKEN.strip()
83 settings = TwitchTokenManagerSettings(
84 validate_timeout_seconds=TWITCH_TOKEN_VALIDATE_TIMEOUT_SECONDS,
85 refresh_timeout_seconds=TWITCH_TOKEN_REFRESH_TIMEOUT_SECONDS,
86 )
87 if not refresh_token:
88 return TwitchTokenManager(
89 access_token=user_token,
90 settings=settings,
91 observability=observability,
92 logger=logger,
93 )
95 client_id = CLIENT_ID or require_env("TWITCH_CLIENT_ID")
96 client_secret = resolve_client_secret_for_irc_refresh()
97 if not client_secret:
98 logger.warning(
99 "TWITCH_REFRESH_TOKEN definido, mas TWITCH_CLIENT_SECRET nao encontrado. "
100 "Refresh automatico do token nao funcionara. Configure TWITCH_CLIENT_SECRET na Host."
101 )
103 return TwitchTokenManager(
104 access_token=user_token,
105 refresh_token=refresh_token,
106 client_id=client_id,
107 client_secret=client_secret or "",
108 refresh_margin_seconds=TWITCH_TOKEN_REFRESH_MARGIN_SECONDS,
109 settings=settings,
110 observability=observability,
111 logger=logger,
112 )
115def run_irc_mode() -> None:
116 try:
117 token_manager = build_irc_token_manager()
118 channel_logins = resolve_irc_channel_logins()
119 bot = IrcByteBot(
120 host=TWITCH_IRC_HOST,
121 port=TWITCH_IRC_PORT,
122 use_tls=TWITCH_IRC_TLS,
123 bot_login=TWITCH_BOT_LOGIN or require_env("TWITCH_BOT_LOGIN"),
124 channel_logins=channel_logins,
125 token_manager=token_manager,
126 )
128 async def run_with_channel_control() -> None:
129 running_loop = asyncio.get_running_loop()
130 irc_channel_control.bind(loop=running_loop, bot=bot)
132 async def _check_clips_auth() -> None:
133 # Avoid circular import
134 from bot.control_plane import control_plane
136 try:
137 token_valid, has_clips_edit = await token_manager.validate_clips_auth()
139 config = control_plane.get_config()
140 if config.get("clip_pipeline_enabled") and not has_clips_edit:
141 logger.warning(
142 "Pipeline de clips habilitado mas scope 'clips:edit' ausente no token."
143 )
145 except Exception as error:
146 logger.error("Erro ao validar auth de clips: %s", error)
147 observability.record_error(category="clips_auth", details=str(error))
149 async def verify_clips_auth_loop() -> None:
150 # Initial check
151 await asyncio.sleep(5) # Wait for boot
152 while True:
153 await _check_clips_auth()
154 await asyncio.sleep(3600)
156 async def send_autonomy_chat(text: str) -> None:
157 await bot.send_reply(text)
159 autonomy_runtime.bind(
160 loop=running_loop,
161 mode="irc",
162 auto_chat_dispatcher=send_autonomy_chat,
163 )
165 # Clip Jobs Runtime
166 clip_jobs.bind_token_provider(token_manager.ensure_token_for_connection)
167 clip_jobs.start(running_loop)
169 # Start background task
170 asyncio.create_task(verify_clips_auth_loop())
172 try:
173 await bot.run_forever()
174 finally:
175 clip_jobs.stop()
176 autonomy_runtime.unbind()
177 irc_channel_control.unbind()
179 asyncio.run(run_with_channel_control())
180 except Exception as error:
181 logger.error("Erro fatal no modo IRC: %s", error)
182 observability.record_error(category="bootstrap_irc", details=str(error))
185def run_eventsub_mode() -> None:
186 require_env("TWITCH_CLIENT_ID")
187 require_env("TWITCH_BOT_ID")
188 require_env("TWITCH_CHANNEL_ID")
189 bot = ByteBot(client_secret=get_secret())
190 try:
191 bot.run()
192 finally:
193 autonomy_runtime.unbind()