totelegram 0.9.10__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.
- totelegram/__init__.py +13 -0
- totelegram/cli/__init__.py +0 -0
- totelegram/cli/__main__.py +108 -0
- totelegram/cli/commands/__init__.py +0 -0
- totelegram/cli/commands/backup.py +107 -0
- totelegram/cli/commands/config.py +343 -0
- totelegram/cli/commands/profile.py +401 -0
- totelegram/cli/commands/send.py +99 -0
- totelegram/cli/logic.py +208 -0
- totelegram/cli/ui.py +654 -0
- totelegram/database.py +152 -0
- totelegram/discovery.py +153 -0
- totelegram/identity.py +599 -0
- totelegram/logging_config.py +69 -0
- totelegram/migration.py +73 -0
- totelegram/models.py +527 -0
- totelegram/packaging.py +339 -0
- totelegram/schemas.py +326 -0
- totelegram/stream.py +157 -0
- totelegram/telegram/__init__.py +0 -0
- totelegram/telegram/access.py +226 -0
- totelegram/telegram/auth.py +108 -0
- totelegram/telegram/client.py +202 -0
- totelegram/telegram/patches.py +373 -0
- totelegram/telegram/search.py +90 -0
- totelegram/types.py +41 -0
- totelegram/uploader.py +314 -0
- totelegram/utils.py +389 -0
- totelegram-0.9.10.dist-info/METADATA +70 -0
- totelegram-0.9.10.dist-info/RECORD +33 -0
- totelegram-0.9.10.dist-info/WHEEL +5 -0
- totelegram-0.9.10.dist-info/entry_points.txt +2 -0
- totelegram-0.9.10.dist-info/top_level.txt +1 -0
totelegram/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
totelegram nació como un gestor de archivos para administrar la memoria digital en Telegram, construido sobre una premisa central:
|
|
3
|
+
|
|
4
|
+
La propiedad no es un registro, es una posibilidad.
|
|
5
|
+
|
|
6
|
+
No funciona solo como un uploader, sino como un índice de disponibilidad.
|
|
7
|
+
En este modelo, los registros son globales, porque la propiedad no se define por la cuenta que realiza la acción, sino por el acceso efectivo a la información.
|
|
8
|
+
|
|
9
|
+
Ver: docs/VISION.md
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
__version__ = "0.9.10"
|
|
13
|
+
CURRENT_DB_VERSION = 1
|
|
File without changes
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
import sys
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from totelegram.cli.commands import backup, config, profile
|
|
10
|
+
from totelegram.schemas import CLIState
|
|
11
|
+
from totelegram.utils import APP_NAME, get_user_config_dir
|
|
12
|
+
|
|
13
|
+
logging.getLogger("dotenv").setLevel(logging.CRITICAL)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
from totelegram import __version__
|
|
17
|
+
from totelegram.cli.commands import send
|
|
18
|
+
from totelegram.cli.ui import console
|
|
19
|
+
from totelegram.identity import SettingsManager
|
|
20
|
+
from totelegram.logging_config import setup_logging
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
COMMANDS_IGNORING_USE = ["profile", "version"]
|
|
24
|
+
|
|
25
|
+
app = typer.Typer(
|
|
26
|
+
help="Herramienta para subir archivos a Telegram sin límite de tamaño.",
|
|
27
|
+
add_completion=False,
|
|
28
|
+
no_args_is_help=True,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
app.add_typer(config.app, name="config")
|
|
32
|
+
app.add_typer(profile.app, name="profile")
|
|
33
|
+
app.command(name="send")(send.send_files)
|
|
34
|
+
app.command(name="backup")(backup.backup_folders)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def version_callback(value: bool):
|
|
38
|
+
if value:
|
|
39
|
+
console.print(f"toTelegram [bold cyan]v{__version__}[/bold cyan]")
|
|
40
|
+
raise typer.Exit()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.callback()
|
|
44
|
+
def main(
|
|
45
|
+
ctx: typer.Context,
|
|
46
|
+
use: Optional[str] = typer.Option(
|
|
47
|
+
None,
|
|
48
|
+
"--use",
|
|
49
|
+
"-u",
|
|
50
|
+
help="Perfil a utilizar para esta operación (ignora el perfil activo).",
|
|
51
|
+
is_eager=True,
|
|
52
|
+
),
|
|
53
|
+
version: Optional[bool] = typer.Option(
|
|
54
|
+
None,
|
|
55
|
+
"--version",
|
|
56
|
+
"-V",
|
|
57
|
+
callback=version_callback,
|
|
58
|
+
is_eager=True,
|
|
59
|
+
help="Muestra la versión de la aplicación.",
|
|
60
|
+
),
|
|
61
|
+
debug: bool = typer.Option(
|
|
62
|
+
False,
|
|
63
|
+
"--debug",
|
|
64
|
+
help="Activa el modo debug (DB independiente y logs detallados).",
|
|
65
|
+
),
|
|
66
|
+
):
|
|
67
|
+
"""
|
|
68
|
+
Se ejecuta antes que cualquier comando.
|
|
69
|
+
"""
|
|
70
|
+
worktable = get_user_config_dir(APP_NAME)
|
|
71
|
+
config_manager = SettingsManager(worktable)
|
|
72
|
+
|
|
73
|
+
cmd_name = ctx.invoked_subcommand or "sys"
|
|
74
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
75
|
+
|
|
76
|
+
log_dir = worktable / "logs"
|
|
77
|
+
log_path = log_dir / f"{timestamp}_{cmd_name}.log"
|
|
78
|
+
|
|
79
|
+
setup_logging(log_path, debug)
|
|
80
|
+
|
|
81
|
+
# main resuelve la intencion del nombre del perfil a usar; las validaciones dependen del contexto del comando.
|
|
82
|
+
profile_name = use or config_manager.get_active_profile_name()
|
|
83
|
+
ctx.obj = CLIState(
|
|
84
|
+
manager=config_manager, profile_name=profile_name, is_debug=debug
|
|
85
|
+
)
|
|
86
|
+
logger.debug(f"--- Nueva Ejecución: {sys.argv} ---")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def run_script():
|
|
90
|
+
# Buscamos argumentos que sean exactamente números negativos (IDs de Telegram)
|
|
91
|
+
# "-1001309586477" -> "ID:-1001309586477"
|
|
92
|
+
regex = re.compile(r"^-\d+$")
|
|
93
|
+
if len(sys.argv) > 1:
|
|
94
|
+
sys.argv = [f"ID:{arg}" if regex.match(arg) else arg for arg in sys.argv]
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
from totelegram.cli.__main__ import app
|
|
98
|
+
|
|
99
|
+
app()
|
|
100
|
+
except Exception as e:
|
|
101
|
+
from totelegram.cli.ui import UI
|
|
102
|
+
|
|
103
|
+
UI.error(str(e))
|
|
104
|
+
sys.exit(1)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if __name__ == "__main__":
|
|
108
|
+
app()
|
|
File without changes
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from totelegram.cli.commands.config import _get_config_tools
|
|
8
|
+
from totelegram.cli.logic import (
|
|
9
|
+
InventoryEngine,
|
|
10
|
+
get_or_create_job,
|
|
11
|
+
prepare_upload_context,
|
|
12
|
+
)
|
|
13
|
+
from totelegram.cli.ui import UI, DisplayUpload, console
|
|
14
|
+
from totelegram.schemas import VALUE_NOT_SET, CLIState, Commands
|
|
15
|
+
from totelegram.uploader import UploadService
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def backup_folders(
|
|
19
|
+
ctx: typer.Context,
|
|
20
|
+
paths: List[Path] = typer.Argument(
|
|
21
|
+
...,
|
|
22
|
+
exists=True,
|
|
23
|
+
help="Lista de carpetas a archivar",
|
|
24
|
+
),
|
|
25
|
+
force: bool = typer.Option(
|
|
26
|
+
False,
|
|
27
|
+
"--force",
|
|
28
|
+
"-f",
|
|
29
|
+
help="Fuerza ignorando el estado del archivo en el sistema",
|
|
30
|
+
),
|
|
31
|
+
):
|
|
32
|
+
"""
|
|
33
|
+
Convierte una carpeta en una Cinta de Datos (TAR) y la distribuye en volúmenes.
|
|
34
|
+
"""
|
|
35
|
+
state: CLIState = ctx.obj
|
|
36
|
+
profile_name, _ = _get_config_tools(ctx)
|
|
37
|
+
|
|
38
|
+
settings = state.manager.get_settings(profile_name)
|
|
39
|
+
|
|
40
|
+
if settings.chat_id == VALUE_NOT_SET:
|
|
41
|
+
UI.error("El chat destino no está configurado.")
|
|
42
|
+
commands = [
|
|
43
|
+
f"{Commands.CONFIG_SET} chat_id <ID>",
|
|
44
|
+
f"{Commands.CONFIG_SEARCH} <QUERY>",
|
|
45
|
+
]
|
|
46
|
+
UI.tip("puedes configurarlo usando uno de estos comandos:", commands)
|
|
47
|
+
raise typer.Exit(1)
|
|
48
|
+
|
|
49
|
+
if force:
|
|
50
|
+
UI.warn("Forzando la subida de carpetas sin comprobar el estado del archivo.")
|
|
51
|
+
|
|
52
|
+
# --- Scaneo y Informe ---
|
|
53
|
+
with console.status("[dim]Escaneando directorios...[/]"):
|
|
54
|
+
scan_report = InventoryEngine(settings, force).scan_backup_inventory(paths)
|
|
55
|
+
|
|
56
|
+
DisplayUpload.show_skip_report(scan_report, "carpeta", force_verbose=False)
|
|
57
|
+
|
|
58
|
+
candidates = scan_report.found
|
|
59
|
+
if not candidates:
|
|
60
|
+
UI.warn("No se encontraron carpetas válidas para procesar.")
|
|
61
|
+
UI.print(
|
|
62
|
+
"[dim]Asegúrate de que las rutas existan y no estén excluidas por tus patrones de configuración.[/]"
|
|
63
|
+
)
|
|
64
|
+
raise typer.Exit(0)
|
|
65
|
+
|
|
66
|
+
# --- Subida de lo encontrado ---
|
|
67
|
+
|
|
68
|
+
UI.separator()
|
|
69
|
+
|
|
70
|
+
with state.scope() as (client, db):
|
|
71
|
+
u_ctx = prepare_upload_context(state, client, db, settings)
|
|
72
|
+
uploader = UploadService(u_ctx)
|
|
73
|
+
|
|
74
|
+
from totelegram.telegram.patches import get_patch_status
|
|
75
|
+
|
|
76
|
+
status = get_patch_status()
|
|
77
|
+
if status["applied"]:
|
|
78
|
+
UI.success("Core Engine: Pyrogram Runtime Patches [ACTIVE]")
|
|
79
|
+
else:
|
|
80
|
+
UI.error("Core Engine: Pyrogram Runtime Patches [FAILED]")
|
|
81
|
+
|
|
82
|
+
user = u_ctx.owner.first_name or u_ctx.owner.username
|
|
83
|
+
chat_n = u_ctx.tg_chat.title or u_ctx.tg_chat.username
|
|
84
|
+
UI.success(f"Conectado como [bold]{user}[/]")
|
|
85
|
+
UI.info(f"Destino: [bold cyan]{chat_n}[/] [dim](ID: {u_ctx.tg_chat.id})[/]")
|
|
86
|
+
UI.print("", indent=False)
|
|
87
|
+
|
|
88
|
+
for index, folder in enumerate(candidates, 1):
|
|
89
|
+
is_last = index == len(candidates)
|
|
90
|
+
DisplayUpload.show_backup_header(folder.name, index, len(candidates))
|
|
91
|
+
|
|
92
|
+
with UI.loading("Analizando contenido..."):
|
|
93
|
+
report_internal = InventoryEngine(settings).scan_backup_internal(folder)
|
|
94
|
+
time.sleep(0.3)
|
|
95
|
+
|
|
96
|
+
DisplayUpload.show_internal_scan_result(report_internal)
|
|
97
|
+
|
|
98
|
+
job = get_or_create_job(folder, u_ctx, force, is_last)
|
|
99
|
+
if job is None:
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
if uploader.process_job(job, folder):
|
|
103
|
+
UI.success(f"Carpeta [bold]{folder.name}[/] procesada exitosamente.")
|
|
104
|
+
|
|
105
|
+
if scan_report.skipped_by_integrity:
|
|
106
|
+
UI.separator()
|
|
107
|
+
DisplayUpload.show_integrity_advice(scan_report)
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from typing import TYPE_CHECKING, List, Literal, Tuple, cast
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from totelegram.cli.ui import UI, DisplayConfig, DisplayGeneric, DisplayProfile
|
|
9
|
+
from totelegram.identity import ConfigService
|
|
10
|
+
from totelegram.schemas import VALUE_NOT_SET, CLIState, Commands
|
|
11
|
+
from totelegram.telegram.access import ChatAccessService
|
|
12
|
+
from totelegram.telegram.client import TelegramSession
|
|
13
|
+
from totelegram.telegram.search import ChatSearchService
|
|
14
|
+
from totelegram.utils import (
|
|
15
|
+
is_direct_identifier,
|
|
16
|
+
is_potential_username,
|
|
17
|
+
is_suspected_glob_expansion,
|
|
18
|
+
normalize_chat_id,
|
|
19
|
+
validate_item,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from pyrogram import Client # type: ignore
|
|
24
|
+
from pyrogram.types import User
|
|
25
|
+
|
|
26
|
+
app = typer.Typer(help="Configuración del perfil actual.")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def handle_config_errors(func):
|
|
30
|
+
@wraps(func)
|
|
31
|
+
def wrapper(ctx: typer.Context, *args, **kwargs):
|
|
32
|
+
try:
|
|
33
|
+
return func(ctx, *args, **kwargs)
|
|
34
|
+
except ValueError as e:
|
|
35
|
+
UI.error(str(e))
|
|
36
|
+
raise typer.Exit(1)
|
|
37
|
+
|
|
38
|
+
return wrapper
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _get_config_tools(ctx: typer.Context) -> Tuple[str, ConfigService]:
|
|
42
|
+
"""Extrae las herramientas necesarias para cualquier comando de configuracion que requieran un perfil."""
|
|
43
|
+
state: CLIState = ctx.obj
|
|
44
|
+
settings_name = cast(str, state.manager.resolve_profile_name(state.profile_name))
|
|
45
|
+
DisplayProfile.announce_profile_used(settings_name)
|
|
46
|
+
service = ConfigService(state.manager, state.is_debug)
|
|
47
|
+
return settings_name, service
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@handle_config_errors
|
|
51
|
+
def _command_modify_list(
|
|
52
|
+
settings_name: str,
|
|
53
|
+
service: ConfigService,
|
|
54
|
+
key: str,
|
|
55
|
+
values: List[str],
|
|
56
|
+
action: Literal["add", "remove"],
|
|
57
|
+
):
|
|
58
|
+
updates = service.prepare_updates([key, values]) # type: ignore
|
|
59
|
+
verb = "agregar" if action == "add" else "eliminar"
|
|
60
|
+
|
|
61
|
+
if is_suspected_glob_expansion(values):
|
|
62
|
+
if not DisplayConfig.confirm_expanded_pattern(verb, key, values):
|
|
63
|
+
UI.info("Operacion cancelada.")
|
|
64
|
+
raise typer.Exit()
|
|
65
|
+
|
|
66
|
+
for k, val in updates.items():
|
|
67
|
+
changed, final_val = service.apply_update(settings_name, k, val, action=action)
|
|
68
|
+
if changed:
|
|
69
|
+
UI.success(
|
|
70
|
+
f"Configuracion [bold]{k}[/] actualizada: {json.dumps(final_val)}"
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
UI.info(f"Configuracion [bold]{k}[/] no sufrio cambios.")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@app.callback(invoke_without_command=True)
|
|
77
|
+
def main(ctx: typer.Context):
|
|
78
|
+
"""Muestra la configuración actual si no se pasa un subcomando."""
|
|
79
|
+
if ctx.invoked_subcommand is not None:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
state: CLIState = ctx.obj
|
|
83
|
+
manager = state.manager
|
|
84
|
+
profile_name = manager.resolve_profile_name(state.profile_name, strict=False)
|
|
85
|
+
if profile_name:
|
|
86
|
+
settings = manager.get_settings(profile_name)
|
|
87
|
+
DisplayConfig.show_config_table(profile_name, manager, state.is_debug, settings)
|
|
88
|
+
|
|
89
|
+
commands = [
|
|
90
|
+
f"{Commands.CONFIG_SET} <KEY> <VALUE>",
|
|
91
|
+
f"{Commands.CONFIG_EDIT_LIST} <KEY> <VALUE1>",
|
|
92
|
+
]
|
|
93
|
+
UI.tip(
|
|
94
|
+
"Puedes modificar cualquier configuración usando los siguientes comandos:",
|
|
95
|
+
commands=commands,
|
|
96
|
+
spacing="top",
|
|
97
|
+
)
|
|
98
|
+
else:
|
|
99
|
+
UI.warn("No hay un perfil activo para mostrar valores de configuracion.")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@app.command(name="set")
|
|
103
|
+
@handle_config_errors
|
|
104
|
+
def set_configs(
|
|
105
|
+
ctx: typer.Context,
|
|
106
|
+
args: List[str] = typer.Argument(
|
|
107
|
+
None, help="Pares de CLAVE VALOR (ej: chat_id 12345 upload_limit_rate_kbps 500)"
|
|
108
|
+
),
|
|
109
|
+
):
|
|
110
|
+
"""
|
|
111
|
+
Modifica una o varias configuraciones al mismo tiempo.
|
|
112
|
+
Uso: totelegram config set chat_id 999999 upload_limit_rate_kbps 1000
|
|
113
|
+
|
|
114
|
+
NOTA:
|
|
115
|
+
Evitar implementar lógica de red o resolución de Telegram (como 'config resolve')
|
|
116
|
+
dentro de este comando por las siguientes razones:
|
|
117
|
+
|
|
118
|
+
1. INDEPENDENCIA: 'config set' debe funcionar 100% offline. Su única responsabilidad
|
|
119
|
+
es la persistencia en disco (.env). No debe depender de una sesión de Pyrogram.
|
|
120
|
+
2. ATOMICIDAD: Este comando puede recibir múltiples pares clave-valor.
|
|
121
|
+
Añadir banderas como '--verify' o '--resolve' crearía ambigüedad sobre qué
|
|
122
|
+
campo se está verificando o resolviendo.
|
|
123
|
+
3. PREDICTIBILIDAD: Para automatización, 'set' debe ser instantáneo.
|
|
124
|
+
Cualquier validación de red debe delegarse al comando 'config check' o
|
|
125
|
+
'config resolve'.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
settings_name, service = _get_config_tools(ctx)
|
|
129
|
+
updates = service.prepare_updates(args)
|
|
130
|
+
for key, val in updates.items():
|
|
131
|
+
changed, final_val = service.apply_update(settings_name, key, val, action="set")
|
|
132
|
+
if changed:
|
|
133
|
+
UI.success(
|
|
134
|
+
f"Configuracion [bold]{key}[/] actualizada con [bold]{final_val}[/]."
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
UI.info(f"Configuracion [bold]{key}[/] ya tiene ese valor.")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@app.command("unset")
|
|
141
|
+
@handle_config_errors
|
|
142
|
+
def unset_config(
|
|
143
|
+
ctx: typer.Context,
|
|
144
|
+
key: str = typer.Argument(..., help="Clave a restaurar a su valor por defecto"),
|
|
145
|
+
):
|
|
146
|
+
"""Quita una configuración personalizada para usar el valor por defecto."""
|
|
147
|
+
|
|
148
|
+
settings_name, service = _get_config_tools(ctx)
|
|
149
|
+
default_val = service.restore_default(settings_name, key)
|
|
150
|
+
UI.success(f"Restaurado [bold]{key}[/] al valor por defecto: {default_val}")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@app.command("add")
|
|
154
|
+
def add_config(
|
|
155
|
+
ctx: typer.Context,
|
|
156
|
+
key: str,
|
|
157
|
+
values: List[str] = typer.Argument(
|
|
158
|
+
..., min=1, callback=lambda x: [validate_item(i) for i in x]
|
|
159
|
+
),
|
|
160
|
+
):
|
|
161
|
+
"""Agrega valores a una lista (ej. exclude_files)."""
|
|
162
|
+
settings_name, service = _get_config_tools(ctx)
|
|
163
|
+
_command_modify_list(settings_name, service, key, values, "add")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@app.command("remove")
|
|
167
|
+
def remove_config(
|
|
168
|
+
ctx: typer.Context,
|
|
169
|
+
key: str,
|
|
170
|
+
values: List[str] = typer.Argument(
|
|
171
|
+
..., min=1, callback=lambda x: [validate_item(i) for i in x]
|
|
172
|
+
),
|
|
173
|
+
):
|
|
174
|
+
"""Quita valores a una lista (ej. exclude_files)."""
|
|
175
|
+
settings_name, service = _get_config_tools(ctx)
|
|
176
|
+
_command_modify_list(settings_name, service, key, values, "remove")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@app.command("check")
|
|
180
|
+
@handle_config_errors
|
|
181
|
+
def check_config(ctx: typer.Context):
|
|
182
|
+
"""Verifica que el perfil actual esté listo para subir archivos."""
|
|
183
|
+
state: CLIState = ctx.obj
|
|
184
|
+
manager = state.manager
|
|
185
|
+
|
|
186
|
+
profile_name = manager.resolve_profile_name(state.profile_name, strict=False)
|
|
187
|
+
if not profile_name:
|
|
188
|
+
UI.error("No hay un perfil activo o el perfil especificado no existe.")
|
|
189
|
+
raise typer.Exit(1)
|
|
190
|
+
|
|
191
|
+
with UI.loading(
|
|
192
|
+
f"Comprobando integridad local del perfil: [bold]{profile_name}[/]"
|
|
193
|
+
):
|
|
194
|
+
time.sleep(0.2)
|
|
195
|
+
profile = manager.get_profile(profile_name)
|
|
196
|
+
|
|
197
|
+
if not profile:
|
|
198
|
+
UI.error(f"Perfil '{profile_name}' no encontrado.")
|
|
199
|
+
raise typer.Exit(1)
|
|
200
|
+
|
|
201
|
+
if not profile.is_trinity:
|
|
202
|
+
UI.error(f"El perfil '{profile_name}' está incompleto.")
|
|
203
|
+
if not profile.has_env:
|
|
204
|
+
UI.info("Falta el archivo de configuración (.env).")
|
|
205
|
+
if not profile.has_session:
|
|
206
|
+
UI.info("Falta el archivo de sesión de Telegram (.session).")
|
|
207
|
+
|
|
208
|
+
command = f"{Commands.PROFILE_CREATE} --force"
|
|
209
|
+
UI.tip(
|
|
210
|
+
f"Créalo de nuevo usando el siguiente comando:",
|
|
211
|
+
commands=command,
|
|
212
|
+
spacing="top",
|
|
213
|
+
)
|
|
214
|
+
raise typer.Exit(1)
|
|
215
|
+
|
|
216
|
+
settings = manager.get_settings(profile_name)
|
|
217
|
+
|
|
218
|
+
if settings.chat_id == VALUE_NOT_SET:
|
|
219
|
+
UI.warn("El destino (chat_id) no está configurado.")
|
|
220
|
+
UI.tip(
|
|
221
|
+
"Configura el destino usando uno de estos comandos:",
|
|
222
|
+
commands=[
|
|
223
|
+
f"{Commands.CONFIG_SET} chat_id <ID>",
|
|
224
|
+
f"{Commands.CONFIG_SEARCH} <NOMBRE>",
|
|
225
|
+
],
|
|
226
|
+
)
|
|
227
|
+
raise typer.Exit(code=1)
|
|
228
|
+
|
|
229
|
+
with TelegramSession.from_profile(profile_name, manager) as client:
|
|
230
|
+
with UI.loading("Verificando conexión y permisos en Telegram..."):
|
|
231
|
+
validator = ChatAccessService(client)
|
|
232
|
+
access_report = validator.verify_access(settings.chat_id)
|
|
233
|
+
|
|
234
|
+
if not access_report.is_ready:
|
|
235
|
+
UI.warn("No tienes permisos de escritura en el chat destino.")
|
|
236
|
+
UI.info(f"Motivo: {access_report.reason}")
|
|
237
|
+
if access_report.hint:
|
|
238
|
+
UI.info(access_report.hint)
|
|
239
|
+
raise typer.Exit(code=1)
|
|
240
|
+
|
|
241
|
+
UI.success(f"Perfil '{profile_name}' verificado y listo para operar.")
|
|
242
|
+
if access_report.chat:
|
|
243
|
+
UI.info(
|
|
244
|
+
f"Destino confirmado: [bold]{access_report.chat.title}[/] "
|
|
245
|
+
f"[dim](ID: {access_report.chat.id})[/dim]"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@app.command("search")
|
|
250
|
+
def search_config(
|
|
251
|
+
ctx: typer.Context,
|
|
252
|
+
query: str = typer.Argument(..., help="Nombre, @username o ID."),
|
|
253
|
+
contains: bool = typer.Option(False, "--contains", "-c"),
|
|
254
|
+
depth: int = typer.Option(100, "--depth", "-d"),
|
|
255
|
+
apply: bool = typer.Option(False, "--apply", "-a"),
|
|
256
|
+
):
|
|
257
|
+
"""Busca y establece el chat de destino."""
|
|
258
|
+
state: CLIState = ctx.obj
|
|
259
|
+
manager = state.manager
|
|
260
|
+
profile_name = cast(str, manager.resolve_profile_name(state.profile_name))
|
|
261
|
+
|
|
262
|
+
DisplayProfile.announce_profile_used(profile_name)
|
|
263
|
+
|
|
264
|
+
# Obtener el ID del chat destino
|
|
265
|
+
with TelegramSession.from_profile(profile_name, manager) as client:
|
|
266
|
+
chat_access = ChatAccessService(client)
|
|
267
|
+
chat_searcher = ChatSearchService(client)
|
|
268
|
+
|
|
269
|
+
me = cast("User", client.get_me())
|
|
270
|
+
UI.info(f"Telegram Session: [bold]{me.username or me.first_name}[/]")
|
|
271
|
+
|
|
272
|
+
chat_id = None
|
|
273
|
+
|
|
274
|
+
if is_direct_identifier(query):
|
|
275
|
+
chat_id = normalize_chat_id(query)
|
|
276
|
+
else:
|
|
277
|
+
is_exact = not contains
|
|
278
|
+
search_desc = (
|
|
279
|
+
f"llamado exactamente '[bold]{query}[/]'"
|
|
280
|
+
if is_exact
|
|
281
|
+
else f"que contenga '[bold]{query}[/]' (sin distinción de mayúsculas y minúsculas)"
|
|
282
|
+
)
|
|
283
|
+
UI.info(f"Buscando chat {search_desc}")
|
|
284
|
+
with UI.loading("Explorando los chats recientes..."):
|
|
285
|
+
result = chat_searcher.search_by_name(query, depth, is_exact)
|
|
286
|
+
|
|
287
|
+
if result.is_resolved and result.winner:
|
|
288
|
+
chat_id = result.winner.id
|
|
289
|
+
|
|
290
|
+
elif result.is_ambiguous:
|
|
291
|
+
conflicts = len(result.conflicts)
|
|
292
|
+
UI.warn(f"Ambigüedad: Hay {conflicts} chats con ese nombre.")
|
|
293
|
+
DisplayGeneric.show_chat_table(
|
|
294
|
+
result.conflicts, "Conflictos Encontrados"
|
|
295
|
+
)
|
|
296
|
+
UI.info("Usa el ID exacto: [bold]config set chat_id <ID>[/]")
|
|
297
|
+
raise typer.Exit(1)
|
|
298
|
+
|
|
299
|
+
elif result.needs_help:
|
|
300
|
+
|
|
301
|
+
DisplayGeneric.show_chat_table(
|
|
302
|
+
result.suggestions, "Quizás quisiste decir:"
|
|
303
|
+
)
|
|
304
|
+
UI.info(
|
|
305
|
+
"Tip: Si no encuentras lo que buscas, intenta aumentar la profundidad con --depth."
|
|
306
|
+
)
|
|
307
|
+
raise typer.Exit(1)
|
|
308
|
+
else:
|
|
309
|
+
UI.error(
|
|
310
|
+
f"No se encontró ningún chat relacionado con '{result.query}' en los primeros {result.search_depth} chats recientes."
|
|
311
|
+
)
|
|
312
|
+
if is_potential_username(result.query):
|
|
313
|
+
UI.tip(
|
|
314
|
+
message="Puedes hacer una busqueda más profunda con [bold]--depth[/] o especificar el chat directamente con @username o t.me/username.",
|
|
315
|
+
commands=[
|
|
316
|
+
f"{Commands.CONFIG_SEARCH} {result.query} --depth 200",
|
|
317
|
+
f"{Commands.CONFIG_SEARCH} @{result.query}",
|
|
318
|
+
],
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
raise typer.Exit(1)
|
|
322
|
+
|
|
323
|
+
# Verificación de permisos
|
|
324
|
+
with UI.loading("Verificando permisos..."):
|
|
325
|
+
report = chat_access.verify_access(chat_id)
|
|
326
|
+
|
|
327
|
+
if not (report.is_ready and report.chat):
|
|
328
|
+
UI.warn("No tienes permisos de escritura en el chat.")
|
|
329
|
+
UI.warn("Corrige los permisos antes de intentar cualquier subida.")
|
|
330
|
+
UI.warn("Configuración 'chat_id' no actualizada.")
|
|
331
|
+
raise typer.Exit(1)
|
|
332
|
+
|
|
333
|
+
# Si llegamos aquí, todo salió bien
|
|
334
|
+
UI.success(
|
|
335
|
+
f"¡Encontrado! [bold]{report.chat.title}[/] [dim](ID: {report.chat.id})[/]"
|
|
336
|
+
)
|
|
337
|
+
UI.success("Tienes permisos de escritura.")
|
|
338
|
+
|
|
339
|
+
if apply:
|
|
340
|
+
manager.set_setting(profile_name, "chat_id", report.chat.id)
|
|
341
|
+
UI.success("Configuración 'chat_id' actualizada.")
|
|
342
|
+
|
|
343
|
+
raise typer.Exit(0)
|