totelegram 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. tests_real/__init__.py +0 -0
  2. tests_real/base.py +311 -0
  3. tests_real/setup_test_env.py +337 -0
  4. tests_real/test_real_file_pipeline.py +149 -0
  5. tests_real/test_real_resumption.py +240 -0
  6. tests_real/test_real_tape_restore.py +206 -0
  7. totelegram/__init__.py +2 -0
  8. totelegram/cli/__init__.py +0 -0
  9. totelegram/cli/__main__.py +149 -0
  10. totelegram/cli/commands/__init__.py +0 -0
  11. totelegram/cli/commands/auth.py +446 -0
  12. totelegram/cli/commands/backup.py +53 -0
  13. totelegram/cli/commands/config.py +308 -0
  14. totelegram/cli/commands/download.py +185 -0
  15. totelegram/cli/commands/forward.py +172 -0
  16. totelegram/cli/commands/index.py +176 -0
  17. totelegram/cli/commands/prune.py +133 -0
  18. totelegram/cli/commands/send.py +48 -0
  19. totelegram/cli/commands/snapshot.py +145 -0
  20. totelegram/cli/commands/target.py +107 -0
  21. totelegram/cli/logic.py +638 -0
  22. totelegram/cli/observer.py +361 -0
  23. totelegram/cli/state.py +82 -0
  24. totelegram/cli/ui.py +241 -0
  25. totelegram/cli/views/__init__.py +1 -0
  26. totelegram/cli/views/errors.py +47 -0
  27. totelegram/cli/views/ledger.py +106 -0
  28. totelegram/cli/views/settings.py +325 -0
  29. totelegram/cli/views/snapshots.py +250 -0
  30. totelegram/cli/views/upload.py +208 -0
  31. totelegram/common/__init__.py +0 -0
  32. totelegram/common/files.py +56 -0
  33. totelegram/common/helpers.py +58 -0
  34. totelegram/common/streams.py +61 -0
  35. totelegram/concurrency.py +660 -0
  36. totelegram/database.py +169 -0
  37. totelegram/discovery.py +403 -0
  38. totelegram/engine/__init__.py +5 -0
  39. totelegram/engine/downloader.py +539 -0
  40. totelegram/engine/events.py +208 -0
  41. totelegram/engine/factory.py +125 -0
  42. totelegram/engine/forwarder.py +252 -0
  43. totelegram/engine/pipeline.py +496 -0
  44. totelegram/engine/planner.py +127 -0
  45. totelegram/engine/relocator.py +142 -0
  46. totelegram/engine/uploader.py +149 -0
  47. totelegram/exceptions.py +312 -0
  48. totelegram/identity.py +613 -0
  49. totelegram/logging_config.py +72 -0
  50. totelegram/migration.py +297 -0
  51. totelegram/models.py +817 -0
  52. totelegram/packaging/__init__.py +4 -0
  53. totelegram/packaging/auditor.py +480 -0
  54. totelegram/packaging/coordinates.py +52 -0
  55. totelegram/packaging/importer.py +1047 -0
  56. totelegram/packaging/indexer.py +118 -0
  57. totelegram/packaging/partitioner.py +109 -0
  58. totelegram/packaging/publisher.py +116 -0
  59. totelegram/packaging/schemas.py +85 -0
  60. totelegram/packaging/snapshot.py +209 -0
  61. totelegram/packaging/ttape.py +266 -0
  62. totelegram/schemas.py +238 -0
  63. totelegram/stream.py +31 -0
  64. totelegram/telegram/__init__.py +0 -0
  65. totelegram/telegram/access.py +233 -0
  66. totelegram/telegram/auth.py +45 -0
  67. totelegram/telegram/captions.py +166 -0
  68. totelegram/telegram/client.py +155 -0
  69. totelegram/telegram/patches.py +464 -0
  70. totelegram/telegram/resolver.py +286 -0
  71. totelegram/telegram/search.py +131 -0
  72. totelegram/telegram/types.py +137 -0
  73. totelegram/telegram/utils.py +77 -0
  74. totelegram/types.py +105 -0
  75. totelegram/utils.py +78 -0
  76. totelegram-0.1.0.dist-info/METADATA +195 -0
  77. totelegram-0.1.0.dist-info/RECORD +80 -0
  78. totelegram-0.1.0.dist-info/WHEEL +5 -0
  79. totelegram-0.1.0.dist-info/entry_points.txt +2 -0
  80. totelegram-0.1.0.dist-info/top_level.txt +2 -0
tests_real/__init__.py ADDED
File without changes
tests_real/base.py ADDED
@@ -0,0 +1,311 @@
1
+ import hashlib
2
+ import json
3
+ import logging
4
+ import os
5
+ import shutil
6
+ import tempfile
7
+ import time
8
+ import unittest
9
+ from pathlib import Path
10
+ from typing import cast
11
+
12
+ import peewee
13
+ from pyrogram.client import Client
14
+ from pyrogram.errors import RPCError
15
+ from pyrogram.types import Chat, Message, User
16
+ from rich.panel import Panel
17
+
18
+ from totelegram.cli.ui import console
19
+ from totelegram.concurrency import LOCK_ROOT_TAG, ConcurrencyCoordinator
20
+ from totelegram.database import DatabaseSession
21
+ from totelegram.discovery import DiscoveryService
22
+ from totelegram.engine.events import UploadObserver
23
+ from totelegram.identity import Settings, SettingsManager
24
+ from totelegram.models import TelegramChat, TelegramUser
25
+ from totelegram.telegram.client import TelegramSession
26
+ from totelegram.types import UploadSession
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ TEST_WORKSPACE = Path.home() / ".config" / "toTelegram_test"
31
+ TEST_PROFILE_NAME = "test_runner"
32
+
33
+
34
+ class RealTelegramTestCase(unittest.TestCase):
35
+ """
36
+ TestCase base que encapsula la sesión real de Telegram y la base de datos de prueba.
37
+ """
38
+
39
+ manager: SettingsManager
40
+ settings: Settings
41
+ client: Client
42
+ db_session: DatabaseSession
43
+ db: peewee.Database
44
+ tg_chat: Chat
45
+ owner: TelegramUser
46
+ test_chat_id: int
47
+
48
+ @classmethod
49
+ def _skip_with_panel(cls, reason: str, details: str, command: str | None = None):
50
+ """Muestra un panel visual explicativo antes de registrar el SkipTest en unittest."""
51
+ lines = [f"[yellow]{reason}[/yellow]\n", f"[dim]{details}[/dim]"]
52
+ if command:
53
+ lines.append("\nEjecuta el siguiente comando para aprovisionar el entorno:")
54
+ lines.append(f" [bold cyan]> {command}[/bold cyan]")
55
+
56
+ panel = Panel(
57
+ "\n".join(lines),
58
+ title="[bold yellow]TESTS REALES OMITIDOS (ENTORNO PENDIENTE)[/bold yellow]",
59
+ border_style="yellow",
60
+ padding=(1, 2),
61
+ )
62
+ console.print()
63
+ console.print(panel)
64
+ console.print()
65
+ raise unittest.SkipTest(reason)
66
+
67
+ @classmethod
68
+ def setUpClass(cls):
69
+ """Inicializa la sesión real de Telegram y la base de datos aislada."""
70
+ # Comprobar si el workspace existe
71
+ if not TEST_WORKSPACE.exists():
72
+ cls._skip_with_panel(
73
+ reason=f"Entorno de pruebas no configurado en: {TEST_WORKSPACE}",
74
+ details="Estos tests realizan transferencias reales y requieren una sesión y canal dedicados.",
75
+ command="python3 tests_real/setup_test_env.py",
76
+ )
77
+
78
+ cls.manager = SettingsManager(TEST_WORKSPACE)
79
+ profile = cls.manager.get_profile(TEST_PROFILE_NAME)
80
+
81
+ # Comprobar si el perfil está completo
82
+ if not profile or not profile.is_trinity:
83
+ cls._skip_with_panel(
84
+ reason=f"El perfil '{TEST_PROFILE_NAME}' no existe o está incompleto en {TEST_WORKSPACE}.",
85
+ details="Falta el archivo de sesión .session o la configuración .json.",
86
+ command="python3 tests_real/setup_test_env.py",
87
+ )
88
+
89
+ cls.settings = cls.manager.get_settings(TEST_PROFILE_NAME)
90
+
91
+ # Iniciar cliente Pyrogram
92
+ try:
93
+ cls._tg_session_ctx = TelegramSession(
94
+ session_name=TEST_PROFILE_NAME,
95
+ api_id=cls.settings.api_id,
96
+ api_hash=cls.settings.api_hash,
97
+ profiles_dir=cls.manager.profiles_dir,
98
+ )
99
+ cls.client = cls._tg_session_ctx.__enter__()
100
+ except Exception as e:
101
+ cls._skip_with_panel(
102
+ reason=f"No se pudo conectar a Telegram: {e}",
103
+ details="Verifica tu conexión a Internet o que la sesión no haya sido revocada.",
104
+ command="python3 tests_real/setup_test_env.py",
105
+ )
106
+
107
+ # Iniciar sesión de base de datos aislada
108
+ cls.db_session = DatabaseSession(
109
+ cls.manager.database_path, auto_init_schema=True
110
+ )
111
+ cls.db = cls.db_session.__enter__()
112
+
113
+ # Resolver chat de pruebas en orden de prioridad:
114
+ # 1. Archivo dedicado test_chat.json
115
+ # 2. Variable de entorno TOTELEGRAM_TEST_CHAT_ID o TOTELEGRAM_TARGET
116
+ # 3. Registro previo en la tabla TelegramChat de SQLite
117
+ test_chat_id = None
118
+ target_file = TEST_WORKSPACE / "test_chat.json"
119
+ if target_file.exists():
120
+ try:
121
+ data = json.loads(target_file.read_text(encoding="utf-8"))
122
+ test_chat_id = data.get("chat_id")
123
+ except Exception as e:
124
+ logger.debug(f"Aviso leyendo test_chat.json: {e}")
125
+
126
+ if not test_chat_id:
127
+ env_target = os.environ.get("TOTELEGRAM_TEST_CHAT_ID") or os.environ.get(
128
+ "TOTELEGRAM_TARGET"
129
+ )
130
+ if env_target:
131
+ try:
132
+ test_chat_id = int(env_target)
133
+ except ValueError:
134
+ pass
135
+
136
+ if not test_chat_id:
137
+ try:
138
+ assert TelegramChat.last_verified is not None
139
+ cached_chat = (
140
+ TelegramChat.select()
141
+ .where((TelegramChat.type == "channel") & (TelegramChat.id != 0))
142
+ .order_by(TelegramChat.last_verified.desc())
143
+ .first()
144
+ )
145
+ if cached_chat:
146
+ test_chat_id = cached_chat.id
147
+ except Exception as e:
148
+ logger.debug(f"Aviso buscando chat en DB de pruebas: {e}")
149
+
150
+ if not test_chat_id:
151
+ cls.tearDownClass()
152
+ cls._skip_with_panel(
153
+ reason="No hay un chat_id de pruebas configurado.",
154
+ details="Se necesita un canal dedicado para verificar subidas.",
155
+ command="python3 tests_real/setup_test_env.py",
156
+ )
157
+
158
+ # Obtener información del usuario y del chat de pruebas
159
+ try:
160
+ cls.test_chat_id = int(test_chat_id)
161
+ cls.tg_chat = cast(Chat, cls.client.get_chat(cls.test_chat_id))
162
+ time.sleep(1)
163
+ me = cast(User, cls.client.get_me())
164
+ cls.owner = TelegramUser.get_or_create_from_tg(me)
165
+ except RPCError as e:
166
+ cls.tearDownClass()
167
+ cls._skip_with_panel(
168
+ reason=f"Error validando el chat o cuenta de pruebas en Telegram: {e}",
169
+ details="Asegúrate de que la cuenta tenga acceso al chat configurado.",
170
+ command="python3 tests_real/setup_test_env.py",
171
+ )
172
+
173
+ @classmethod
174
+ def tearDownClass(cls):
175
+ """Cierra de forma segura el cliente de Telegram y la base de datos."""
176
+ if hasattr(cls, "db_session"):
177
+ try:
178
+ cls.db_session.__exit__(None, None, None)
179
+ except Exception as e:
180
+ logger.debug(f"Aviso cerrando DB de test: {e}")
181
+
182
+ if hasattr(cls, "_tg_session_ctx"):
183
+ try:
184
+ cls._tg_session_ctx.__exit__(None, None, None)
185
+ except Exception as e:
186
+ logger.debug(f"Aviso cerrando cliente Telegram: {e}")
187
+
188
+ def setUp(self):
189
+ """Preparación antes de cada método de test."""
190
+ self.created_message_ids: list[int] = []
191
+ self._temp_dirs_to_clean: list[Path] = []
192
+
193
+ # Auto-sanación: Limpiar posibles locks residuales en 'me' de tests previos abortados
194
+ self._heal_dangling_account_locks()
195
+
196
+ def tearDown(self):
197
+ """
198
+ Limpieza post-test: Garantiza la política de 'Basura Cero'
199
+ eliminando todos los mensajes creados en Telegram y carpetas temporales.
200
+ """
201
+ # Eliminar mensajes subidos a Telegram
202
+ if self.created_message_ids:
203
+ try:
204
+ unique_ids = list(set(self.created_message_ids))
205
+ for i in range(0, len(unique_ids), 100):
206
+ batch = unique_ids[i : i + 100]
207
+ self.client.delete_messages(
208
+ chat_id=self.test_chat_id,
209
+ message_ids=batch,
210
+ )
211
+ logger.debug(
212
+ f"Eliminados {len(unique_ids)} mensajes de test en chat {self.test_chat_id}"
213
+ )
214
+ except Exception as e:
215
+ logger.warning(f"Aviso limpiando mensajes en tearDown: {e}")
216
+
217
+ # Limpiar directorios temporales en disco
218
+ for temp_dir in self._temp_dirs_to_clean:
219
+ shutil.rmtree(temp_dir, ignore_errors=True)
220
+
221
+ # HELPERS DE RECOLECCIÓN Y GESTIÓN
222
+
223
+ def track_message(self, message_or_id: Message | int | None):
224
+ """Registra un mensaje para su eliminación automática en tearDown."""
225
+ if message_or_id is None:
226
+ return
227
+ if isinstance(message_or_id, int):
228
+ self.created_message_ids.append(message_or_id)
229
+ elif hasattr(message_or_id, "id"):
230
+ self.created_message_ids.append(message_or_id.id)
231
+
232
+ def track_messages(self, messages_or_ids: list[Message | int]):
233
+ """Registra una lista de mensajes para eliminación automática."""
234
+ for m in messages_or_ids:
235
+ self.track_message(m)
236
+
237
+ def _heal_dangling_account_locks(self):
238
+ """Elimina locks residuales en 'Mensajes Guardados' (me) para evitar falsos AccountBusyError."""
239
+ try:
240
+ query = f"{LOCK_ROOT_TAG} #acc_{self.owner.id}"
241
+ locks = list(self.client.search_messages("me", query=query, limit=5))
242
+ lock_ids = [m.id for m in locks if m and not getattr(m, "empty", False)]
243
+ if lock_ids:
244
+ self.client.delete_messages("me", lock_ids)
245
+ logger.debug(f"Auto-sanados {len(lock_ids)} locks residuales en 'me'.")
246
+ except Exception as e:
247
+ logger.debug(f"Aviso durante auto-sanación de locks: {e}")
248
+
249
+ # FACTORÍAS DE DATOS Y ENTORNO
250
+
251
+ def create_temp_dir(self) -> Path:
252
+ """Crea un directorio temporal que se auto-eliminará en tearDown."""
253
+ temp_dir = Path(tempfile.mkdtemp(prefix="totelegram_test_"))
254
+ self._temp_dirs_to_clean.append(temp_dir)
255
+ return temp_dir
256
+
257
+ def create_dummy_file(
258
+ self,
259
+ filename: str,
260
+ size_bytes: int,
261
+ parent_dir: Path | None = None,
262
+ ) -> Path:
263
+ """
264
+ Crea un archivo binario con contenido pseudoaleatorio determinista y tamaño exacto.
265
+ """
266
+ target_dir = parent_dir or self.create_temp_dir()
267
+ file_path = target_dir / filename
268
+
269
+ chunk_size = 64 * 1024
270
+ written = 0
271
+ with open(file_path, "wb") as f:
272
+ while written < size_bytes:
273
+ to_write = min(chunk_size, size_bytes - written)
274
+ f.write(os.urandom(to_write))
275
+ written += to_write
276
+
277
+ return file_path
278
+
279
+ @staticmethod
280
+ def calculate_md5(file_path: Path) -> str:
281
+ """Calcula el hash MD5 exacto de un archivo en disco."""
282
+ hasher = hashlib.md5()
283
+ with open(file_path, "rb") as f:
284
+ while chunk := f.read(64 * 1024):
285
+ hasher.update(chunk)
286
+ return hasher.hexdigest()
287
+
288
+ def build_upload_session(
289
+ self,
290
+ observer: UploadObserver | None = None,
291
+ ) -> UploadSession:
292
+ """Construye una instancia de UploadSession conectada al arnés de testing."""
293
+ coordinator = ConcurrencyCoordinator(
294
+ db=self.db,
295
+ node_id="test_runner_node",
296
+ client=self.client,
297
+ )
298
+ discovery = DiscoveryService(
299
+ client=self.client,
300
+ db=self.db,
301
+ )
302
+
303
+ return UploadSession(
304
+ client=self.client,
305
+ db=self.db,
306
+ tg_chat=self.tg_chat,
307
+ owner=self.owner,
308
+ settings=self.settings,
309
+ coordinator=coordinator,
310
+ discovery=discovery,
311
+ )
@@ -0,0 +1,337 @@
1
+ """
2
+ Script para aprovisionar, actualizar el canal o limpiar el entorno de pruebas reales.
3
+ Guarda la sesión y ajustes en: ~/.config/toTelegram_test/
4
+ """
5
+
6
+ import argparse
7
+ import json
8
+ import shutil
9
+ import sys
10
+ import tempfile
11
+ import uuid
12
+ from pathlib import Path
13
+
14
+ # Asegurar que el paquete totelegram sea importable desde la raíz del repositorio
15
+ repo_root = Path(__file__).resolve().parent.parent.parent
16
+ if str(repo_root) not in sys.path:
17
+ sys.path.insert(0, repo_root)
18
+
19
+ from pyrogram.errors import RPCError
20
+
21
+ from totelegram.cli.ui import UI, console
22
+ from totelegram.database import DatabaseSession
23
+ from totelegram.identity import (
24
+ DEFAULT_TG_API_HASH,
25
+ DEFAULT_TG_API_ID,
26
+ Settings,
27
+ SettingsManager,
28
+ )
29
+ from totelegram.telegram.access import ChatAccessService
30
+ from totelegram.telegram.auth import AuthLogic
31
+ from totelegram.telegram.client import TelegramSession
32
+ from totelegram.telegram.resolver import TargetResolver
33
+ from totelegram.telegram.types import TargetResolutionStatus
34
+
35
+ TEST_WORKSPACE = Path.home() / ".config" / "toTelegram_test"
36
+ TEST_PROFILE_NAME = "test_runner"
37
+ TEST_TARGET_FILE = TEST_WORKSPACE / "test_chat.json"
38
+
39
+
40
+ def clean_test_env():
41
+ """Cierra la sesión remotamente en los servidores de Telegram y elimina el workspace."""
42
+ console.print(
43
+ "\n[bold cyan]====================================================[/]"
44
+ )
45
+ console.print("[bold cyan] Limpieza del Entorno de Pruebas [/]")
46
+ console.print(
47
+ "[bold cyan]====================================================[/]\n"
48
+ )
49
+
50
+ if not TEST_WORKSPACE.exists():
51
+ console.print(
52
+ "[yellow]No existe un entorno de pruebas configurado en disco.[/]"
53
+ )
54
+ return
55
+
56
+ manager = SettingsManager(TEST_WORKSPACE)
57
+ profile = manager.get_profile(TEST_PROFILE_NAME)
58
+
59
+ if profile and profile.has_session:
60
+ with UI.loading("Revocando sesión remota en los servidores de Telegram..."):
61
+ try:
62
+ settings = manager.get_settings(TEST_PROFILE_NAME)
63
+ with TelegramSession(
64
+ session_name=TEST_PROFILE_NAME,
65
+ api_id=settings.api_id,
66
+ api_hash=settings.api_hash,
67
+ profiles_dir=manager.profiles_dir,
68
+ ) as client:
69
+ client.log_out()
70
+ UI.success("Sesión remota invalidada exitosamente en Telegram.")
71
+ except RPCError as e:
72
+ console.print(f"[dim]Aviso al revocar sesión en Telegram: {e}[/dim]")
73
+ except Exception as e:
74
+ console.print(
75
+ f"[dim]No se pudo revocar la sesión remota ({e}), procediendo con limpieza local...[/dim]"
76
+ )
77
+
78
+ with UI.loading(f"Eliminando workspace local en {TEST_WORKSPACE}..."):
79
+ shutil.rmtree(TEST_WORKSPACE, ignore_errors=True)
80
+
81
+ UI.success("Entorno de pruebas eliminado por completo.\n")
82
+
83
+
84
+ def update_test_channel(query: str | None = None):
85
+ """
86
+ Actualiza exclusivamente el canal de pruebas utilizando la sesión autenticada existente,
87
+ sin requerir volver a ingresar teléfono ni código OTP.
88
+ """
89
+ console.print(
90
+ "\n[bold cyan]====================================================[/]"
91
+ )
92
+ console.print("[bold cyan] Actualización del Canal de Pruebas [/]")
93
+ console.print(
94
+ "[bold cyan]====================================================[/]\n"
95
+ )
96
+
97
+ if not TEST_WORKSPACE.exists():
98
+ UI.error(
99
+ f"No existe un entorno de pruebas en '{TEST_WORKSPACE}'. "
100
+ "Debes realizar la configuración inicial primero."
101
+ )
102
+ sys.exit(1)
103
+
104
+ manager = SettingsManager(TEST_WORKSPACE)
105
+ profile = manager.get_profile(TEST_PROFILE_NAME)
106
+ if not profile or not profile.is_trinity:
107
+ UI.error(
108
+ "El perfil de pruebas no está autenticado. "
109
+ "Ejecuta 'python tests_real/setup_test_env.py' para iniciar sesión."
110
+ )
111
+ sys.exit(1)
112
+
113
+ settings = manager.get_settings(TEST_PROFILE_NAME)
114
+
115
+ with (
116
+ TelegramSession(
117
+ session_name=TEST_PROFILE_NAME,
118
+ api_id=settings.api_id,
119
+ api_hash=settings.api_hash,
120
+ profiles_dir=manager.profiles_dir,
121
+ ) as client,
122
+ DatabaseSession(manager.database_path, auto_init_schema=True) as db,
123
+ ):
124
+ resolver = TargetResolver(client, db)
125
+ access_service = ChatAccessService(client)
126
+
127
+ target_query = query
128
+ while True:
129
+ if not target_query:
130
+ target_query = console.input(
131
+ "Nombre, @username o ID del nuevo canal de pruebas: "
132
+ ).strip()
133
+
134
+ if not target_query:
135
+ console.print("[red]Debes especificar un canal válido.[/]")
136
+ continue
137
+
138
+ with UI.loading(f"Verificando canal '{target_query}'..."):
139
+ resolution = resolver.resolve(target_query)
140
+
141
+ if (
142
+ resolution.status != TargetResolutionStatus.RESOLVED
143
+ or not resolution.chat_id
144
+ ):
145
+ UI.error(resolution.error_message or "No se pudo encontrar el canal.")
146
+ if resolution.hint:
147
+ console.print(f"[dim]{resolution.hint}[/dim]")
148
+ target_query = None
149
+ continue
150
+
151
+ chat_id = resolution.chat_id
152
+
153
+ # Validar permisos de administrador y borrado (requerido para limpieza)
154
+ with UI.loading("Validando permisos de administrador..."):
155
+ access_report = access_service.verify_access(chat_id)
156
+
157
+ if not access_report.is_ready:
158
+ UI.error(f"Problema de acceso: {access_report.reason}")
159
+ if access_report.hint:
160
+ console.print(f"[dim]{access_report.hint}[/dim]")
161
+ target_query = None
162
+ continue
163
+
164
+ can_delete = access_service.can_delete_messages(chat_id)
165
+ if not can_delete:
166
+ UI.warn(
167
+ f"La cuenta tiene acceso a '{resolution.title}', pero [bold red]carece del permiso "
168
+ f"'Eliminar mensajes' (can_delete_messages)[/].\n"
169
+ "Otorga permisos de administrador a la cuenta en los ajustes del canal y presiona Enter para reintentar."
170
+ )
171
+ console.input("\nPresiona Enter tras otorgar los permisos de admin...")
172
+ target_query = None
173
+ continue
174
+
175
+ # Persistir canal en test_chat.json
176
+ TEST_TARGET_FILE.parent.mkdir(parents=True, exist_ok=True)
177
+ TEST_TARGET_FILE.write_text(
178
+ json.dumps(
179
+ {
180
+ "chat_id": chat_id,
181
+ "title": resolution.title,
182
+ },
183
+ indent=2,
184
+ ),
185
+ encoding="utf-8",
186
+ )
187
+
188
+ UI.separator()
189
+ UI.success(
190
+ f"Canal de pruebas actualizado a: [bold cyan]{resolution.title}[/] [dim]({chat_id})[/]"
191
+ )
192
+ console.print(f"[dim]Guardado en: {TEST_TARGET_FILE}[/dim]\n")
193
+ console.print("Ya puedes ejecutar las pruebas:")
194
+ console.print(
195
+ ' [bold yellow]python3 -m unittest discover -s tests_real -p "test_real_*.py" -v[/]\n'
196
+ )
197
+ break
198
+
199
+
200
+ def setup_test_env():
201
+ """Flujo interactivo de aprovisionamiento de cuenta y canal de pruebas."""
202
+ console.print(
203
+ "\n[bold cyan]====================================================[/]"
204
+ )
205
+ console.print(
206
+ "[bold cyan] Configuración del Entorno de Pruebas de toTelegram [/]"
207
+ )
208
+ console.print(
209
+ "[bold cyan]====================================================[/]\n"
210
+ )
211
+
212
+ console.print(
213
+ f"Este asistente configurará una sesión aislada en:\n"
214
+ f"[bold yellow]{TEST_WORKSPACE}[/]\n"
215
+ )
216
+
217
+ manager = SettingsManager(TEST_WORKSPACE)
218
+ existing_profile = manager.get_profile(TEST_PROFILE_NAME)
219
+
220
+ # Si ya existía una sesión válida, ofrecer cambiar solo el canal
221
+ if existing_profile and existing_profile.is_trinity:
222
+ console.print("[green]✔ Se detectó una sesión activa válida para pruebas.[/]")
223
+ console.print("\n¿Qué deseas hacer?")
224
+ console.print(
225
+ " [1] [bold cyan]Actualizar solo el canal de pruebas[/] (Recomendado)"
226
+ )
227
+ console.print(" [2] Reconfigurar cuenta completa (nuevo login OTP)")
228
+ console.print(" [0] Salir")
229
+
230
+ opcion = console.input("\nElige una opción [1]: ").strip()
231
+ if opcion in ("", "1"):
232
+ update_test_channel()
233
+ return
234
+ elif opcion == "0":
235
+ console.print("[yellow]Operación cancelada.[/]")
236
+ return
237
+
238
+ # Credenciales de API
239
+ console.print("\n[bold green]1. Credenciales de Telegram[/]")
240
+ console.print(
241
+ "[dim]Presiona Enter para usar las credenciales compartidas por defecto.[/dim]"
242
+ )
243
+
244
+ api_id_raw = console.input(f"API ID [{DEFAULT_TG_API_ID}]: ").strip()
245
+ api_id = int(api_id_raw) if api_id_raw else DEFAULT_TG_API_ID
246
+
247
+ api_hash_raw = console.input("API HASH [default]: ").strip()
248
+ api_hash = api_hash_raw if api_hash_raw else DEFAULT_TG_API_HASH
249
+
250
+ has_custom_app = (api_id != DEFAULT_TG_API_ID) or (api_hash != DEFAULT_TG_API_HASH)
251
+
252
+ # Login Pyrogram (OTP)
253
+ console.print("\n[bold green]2. Autenticación con Telegram[/]")
254
+ console.print(
255
+ "[dim]Se solicitará tu número de teléfono y el código que recibirás en Telegram.[/dim]\n"
256
+ )
257
+
258
+ temp_session_name = f"temp_setup_{uuid.uuid4().hex[:8]}"
259
+ with tempfile.TemporaryDirectory() as temp_dir:
260
+ auth = AuthLogic(
261
+ session_name=temp_session_name,
262
+ api_id=api_id,
263
+ api_hash=api_hash,
264
+ workdir=Path(temp_dir),
265
+ )
266
+ try:
267
+ temp_session_path, account_id, first_name = auth.run_auth_flow()
268
+ except Exception as e:
269
+ UI.error(f"Fallo durante la autenticación: {e}")
270
+ sys.exit(1)
271
+
272
+ manager.profiles_dir.mkdir(parents=True, exist_ok=True)
273
+ final_session_path = manager.get_session_path(TEST_PROFILE_NAME)
274
+
275
+ if final_session_path.exists():
276
+ final_session_path.unlink()
277
+
278
+ temp_session_path.rename(final_session_path)
279
+
280
+ UI.success(
281
+ f"Sesión autenticada con éxito como: [bold]{first_name}[/] (ID: {account_id})"
282
+ )
283
+
284
+ # Guardar configuración base para testing
285
+ test_settings = Settings(
286
+ profile_name=TEST_PROFILE_NAME,
287
+ telegram_account_id=account_id,
288
+ api_id=api_id,
289
+ api_hash=api_hash,
290
+ has_custom_app=has_custom_app,
291
+ tg_max_size_normal=2 * 1024 * 1024, # 2 MB simulados para pruebas rápidas
292
+ tg_max_size_premium=2 * 1024 * 1024,
293
+ upload_pause_range=[0, 0],
294
+ upload_limit_rate_kbps=0,
295
+ auto_truncate=True,
296
+ )
297
+ manager.save_settings(TEST_PROFILE_NAME, test_settings)
298
+ manager.set_profile_name_as_active(TEST_PROFILE_NAME)
299
+
300
+ # Paso 3: Selección de canal
301
+ update_test_channel()
302
+
303
+
304
+ def main():
305
+ parser = argparse.ArgumentParser(
306
+ description="Aprovisionamiento del entorno de pruebas reales de toTelegram"
307
+ )
308
+ parser.add_argument(
309
+ "-t",
310
+ "--target",
311
+ "--channel",
312
+ dest="target",
313
+ nargs="?",
314
+ const="",
315
+ help="Actualiza únicamente el canal de pruebas sin requerir login ni OTP.",
316
+ )
317
+ parser.add_argument(
318
+ "-c",
319
+ "--clean",
320
+ "--reset",
321
+ action="store_true",
322
+ help="Revoca la sesión remota y elimina el workspace de pruebas.",
323
+ )
324
+
325
+ args = parser.parse_args()
326
+
327
+ if args.clean:
328
+ clean_test_env()
329
+ elif args.target is not None:
330
+ target_value = args.target.strip() if args.target else None
331
+ update_test_channel(target_value)
332
+ else:
333
+ setup_test_env()
334
+
335
+
336
+ if __name__ == "__main__":
337
+ main()