Files
avatar/avatar/telegram_client.py
T
lucianoandClaude Fable 5.1 ff79832c30 AvatarPy: assistente personale con volto 3D, voce, memoria e plugin
Riscrittura in Python dell'assistente Avatar con interfaccia HUD (derivata da Mark LIV, CC BY-NC 4.0, vedi NOTICE.md).
Tre motori (Claude API, server locale OpenAI-compatibile, Claude Code), voce Kokoro/macOS, Whisper MLX,
avatar 3D con sincronizzazione labiale, memoria per categorie, allegati con OCR, monitor con avvisi,
plugin per Calendario, Mail, Promemoria, Note, Musica, app, Mac, timer, meteo, contatti, Messaggi,
file, Comandi Rapidi, browser, Telegram, WhatsApp (archivio e tempo reale).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-23 16:21:39 +02:00

212 lines
8.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Accesso Telegram con l'account dell'utente (Telethon): login e chiamate sincrone per i plugin."""
from __future__ import annotations
import asyncio
import json
import threading
from pathlib import Path
from typing import Any, Awaitable, Callable
from avatar.settings import DATA_DIR, Settings
SESSION = DATA_DIR / "telegram" # Telethon aggiunge .session
LOGIN_STATE = DATA_DIR / "telegram_login.json"
_lock = threading.Lock()
def credentials() -> tuple[int, str]:
s = Settings()
api_id = str(s.get("telegram_api_id") or "").strip()
api_hash = s.get_secret("telegram_api_hash")
if not api_id.isdigit() or not api_hash:
raise RuntimeError("Telegram non configurato: inserisci api id e api hash in Motore e Voce (da my.telegram.org).")
return int(api_id), api_hash
def run(fn: Callable[[Any], Awaitable[Any]], require_auth: bool = True) -> Any:
"""Esegue `fn(client)` in un event loop dedicato, con connessione aperta e chiusa ogni volta."""
from telethon import TelegramClient
api_id, api_hash = credentials()
async def main():
client = TelegramClient(str(SESSION), api_id, api_hash)
await client.connect()
try:
if require_auth and not await client.is_user_authorized():
raise RuntimeError("Telegram: accesso non ancora effettuato. Vai in Motore e Voce e completa l'accesso.")
return await fn(client)
finally:
await client.disconnect()
with _lock:
loop = asyncio.new_event_loop()
try:
return loop.run_until_complete(main())
finally:
loop.close()
# ── Login in due passi (dalla finestra impostazioni) ─────────────────────────
def send_code(phone: str) -> str:
phone = phone.strip().replace(" ", "")
async def go(client):
prev = None
try:
prev = json.loads(LOGIN_STATE.read_text())
except Exception:
pass
if prev and prev.get("phone") == phone and prev.get("hash"):
# Seconda richiesta: Telegram lo rimanda con un altro metodo (SMS o chiamata).
try:
from telethon.tl.functions.auth import ResendCodeRequest
sent = await client(ResendCodeRequest(phone_number=phone, phone_code_hash=prev["hash"]))
LOGIN_STATE.write_text(json.dumps({"phone": phone, "hash": sent.phone_code_hash}))
via = type(sent.type).__name__.replace("SentCodeType", "")
return f"Codice reinviato via {via or 'altro metodo'}: controlla SMS o chiamata, poi inseriscilo qui."
except Exception as err:
LOGIN_STATE.unlink(missing_ok=True)
print(f"[telegram] resend fallito: {err}")
sent = await client.send_code_request(phone)
LOGIN_STATE.write_text(json.dumps({"phone": phone, "hash": sent.phone_code_hash}))
via = type(sent.type).__name__.replace("SentCodeType", "")
dove = "nell'app Telegram (messaggio dal mittente «Telegram»)" if via == "App" else f"via {via}"
return f"Codice inviato {dove}. Inseriscilo qui; se non arriva, premi di nuovo «Invia codice» per riceverlo con un altro metodo."
return run(go, require_auth=False)
def sign_in(code: str, password: str = "") -> str:
try:
st = json.loads(LOGIN_STATE.read_text())
except Exception:
st = None
if st is None or (not code.strip() and password):
if not password:
return "Prima premi «Invia codice» (oppure usa l'accesso con QR); se Telegram chiede la password, scrivila e premi Accedi."
async def only_password(client):
from telethon.errors import SessionPasswordNeededError # noqa: F401
await client.sign_in(password=password)
me = await client.get_me()
LOGIN_STATE.unlink(missing_ok=True)
return f"Accesso effettuato come {me.first_name or ''} {me.last_name or ''} (@{me.username or '—'})."
try:
return run(only_password, require_auth=False)
except Exception as err:
return f"Password non accettata: {err}"
async def go(client):
from telethon.errors import SessionPasswordNeededError
try:
await client.sign_in(st["phone"], code.strip().replace(" ", ""), phone_code_hash=st["hash"])
except SessionPasswordNeededError:
if not password:
return "Serve anche la password di verifica in due passaggi: inseriscila e premi di nuovo Accedi."
await client.sign_in(password=password)
me = await client.get_me()
LOGIN_STATE.unlink(missing_ok=True)
return f"Accesso effettuato come {me.first_name or ''} {me.last_name or ''} (@{me.username or '—'})."
return run(go, require_auth=False)
def status() -> str:
try:
credentials()
except RuntimeError as err:
return str(err)
if not Path(str(SESSION) + ".session").exists():
return "Credenziali presenti; accesso non ancora effettuato."
async def go(client):
if not await client.is_user_authorized():
return "Accesso non ancora effettuato."
me = await client.get_me()
return f"Collegato come {me.first_name or ''} {me.last_name or ''} (@{me.username or '—'})."
try:
return run(go, require_auth=False)
except Exception as err:
return f"Errore: {err}"
def logout() -> str:
async def go(client):
await client.log_out()
return "Disconnesso da Telegram."
try:
return run(go, require_auth=False)
finally:
Path(str(SESSION) + ".session").unlink(missing_ok=True)
# ── Accesso con codice QR (Impostazioni › Dispositivi › Collega dispositivo) ──
_qr_state: dict = {}
def qr_login(on_update: Callable[[str, bytes | None], None]) -> None:
"""Genera un QR, lo passa a `on_update(messaggio, png)` e attende la scansione (fino a 3 minuti).
Bloccante: chiamare da un thread. A ogni scadenza del QR ne genera uno nuovo."""
import io
import qrcode
from telethon import TelegramClient
from telethon.errors import SessionPasswordNeededError
api_id, api_hash = credentials()
async def main():
client = TelegramClient(str(SESSION), api_id, api_hash)
await client.connect()
try:
if await client.is_user_authorized():
me = await client.get_me()
on_update(f"Già collegato come {me.first_name or ''} (@{me.username or '—'}).", None)
return
pwd = _qr_state.get("password", "")
if pwd:
# QR già scansionato in precedenza: manca solo la password.
try:
await client.sign_in(password=pwd)
me = await client.get_me()
on_update(f"Accesso effettuato come {me.first_name or ''} {me.last_name or ''} (@{me.username or '—'}).", None)
return
except Exception:
pass
deadline = asyncio.get_event_loop().time() + 180
while asyncio.get_event_loop().time() < deadline:
try:
qr = await client.qr_login()
except SessionPasswordNeededError:
on_update("QR accettato: ora serve la password di verifica in due passaggi. Scrivila nel campo password e premi «Accedi».", None)
return
buf = io.BytesIO()
qrcode.make(qr.url).save(buf, format="PNG")
on_update("Inquadra il QR dal telefono: Telegram › Impostazioni › Dispositivi › Collega dispositivo desktop.", buf.getvalue())
try:
await qr.wait(timeout=max(5, (qr.expires - qr.expires.__class__.now(qr.expires.tzinfo)).total_seconds()))
break
except asyncio.TimeoutError:
continue
except SessionPasswordNeededError:
pwd = _qr_state.get("password", "")
if not pwd:
on_update("Serve la password di verifica in due passaggi: scrivila nel campo password e ripeti «Accesso con QR».", None)
return
await client.sign_in(password=pwd)
break
if await client.is_user_authorized():
me = await client.get_me()
on_update(f"Accesso effettuato come {me.first_name or ''} {me.last_name or ''} (@{me.username or '—'}).", None)
else:
on_update("QR scaduto senza scansione. Premi di nuovo «Accesso con QR».", None)
finally:
await client.disconnect()
with _lock:
loop = asyncio.new_event_loop()
try:
loop.run_until_complete(main())
finally:
loop.close()