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.
- tests_real/__init__.py +0 -0
- tests_real/base.py +311 -0
- tests_real/setup_test_env.py +337 -0
- tests_real/test_real_file_pipeline.py +149 -0
- tests_real/test_real_resumption.py +240 -0
- tests_real/test_real_tape_restore.py +206 -0
- totelegram/__init__.py +2 -0
- totelegram/cli/__init__.py +0 -0
- totelegram/cli/__main__.py +149 -0
- totelegram/cli/commands/__init__.py +0 -0
- totelegram/cli/commands/auth.py +446 -0
- totelegram/cli/commands/backup.py +53 -0
- totelegram/cli/commands/config.py +308 -0
- totelegram/cli/commands/download.py +185 -0
- totelegram/cli/commands/forward.py +172 -0
- totelegram/cli/commands/index.py +176 -0
- totelegram/cli/commands/prune.py +133 -0
- totelegram/cli/commands/send.py +48 -0
- totelegram/cli/commands/snapshot.py +145 -0
- totelegram/cli/commands/target.py +107 -0
- totelegram/cli/logic.py +638 -0
- totelegram/cli/observer.py +361 -0
- totelegram/cli/state.py +82 -0
- totelegram/cli/ui.py +241 -0
- totelegram/cli/views/__init__.py +1 -0
- totelegram/cli/views/errors.py +47 -0
- totelegram/cli/views/ledger.py +106 -0
- totelegram/cli/views/settings.py +325 -0
- totelegram/cli/views/snapshots.py +250 -0
- totelegram/cli/views/upload.py +208 -0
- totelegram/common/__init__.py +0 -0
- totelegram/common/files.py +56 -0
- totelegram/common/helpers.py +58 -0
- totelegram/common/streams.py +61 -0
- totelegram/concurrency.py +660 -0
- totelegram/database.py +169 -0
- totelegram/discovery.py +403 -0
- totelegram/engine/__init__.py +5 -0
- totelegram/engine/downloader.py +539 -0
- totelegram/engine/events.py +208 -0
- totelegram/engine/factory.py +125 -0
- totelegram/engine/forwarder.py +252 -0
- totelegram/engine/pipeline.py +496 -0
- totelegram/engine/planner.py +127 -0
- totelegram/engine/relocator.py +142 -0
- totelegram/engine/uploader.py +149 -0
- totelegram/exceptions.py +312 -0
- totelegram/identity.py +613 -0
- totelegram/logging_config.py +72 -0
- totelegram/migration.py +297 -0
- totelegram/models.py +817 -0
- totelegram/packaging/__init__.py +4 -0
- totelegram/packaging/auditor.py +480 -0
- totelegram/packaging/coordinates.py +52 -0
- totelegram/packaging/importer.py +1047 -0
- totelegram/packaging/indexer.py +118 -0
- totelegram/packaging/partitioner.py +109 -0
- totelegram/packaging/publisher.py +116 -0
- totelegram/packaging/schemas.py +85 -0
- totelegram/packaging/snapshot.py +209 -0
- totelegram/packaging/ttape.py +266 -0
- totelegram/schemas.py +238 -0
- totelegram/stream.py +31 -0
- totelegram/telegram/__init__.py +0 -0
- totelegram/telegram/access.py +233 -0
- totelegram/telegram/auth.py +45 -0
- totelegram/telegram/captions.py +166 -0
- totelegram/telegram/client.py +155 -0
- totelegram/telegram/patches.py +464 -0
- totelegram/telegram/resolver.py +286 -0
- totelegram/telegram/search.py +131 -0
- totelegram/telegram/types.py +137 -0
- totelegram/telegram/utils.py +77 -0
- totelegram/types.py +105 -0
- totelegram/utils.py +78 -0
- totelegram-0.1.0.dist-info/METADATA +195 -0
- totelegram-0.1.0.dist-info/RECORD +80 -0
- totelegram-0.1.0.dist-info/WHEEL +5 -0
- totelegram-0.1.0.dist-info/entry_points.txt +2 -0
- 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()
|