Files
ordinaceprojekt/Knihovny/telegram_user.py
T
Vladimir Buzalka 2bdac59676 notebookvb
2026-06-14 12:07:35 +02:00

303 lines
12 KiB
Python

"""
telegram_user.py
----------------
Ovládání PLNOHODNOTNÉHO Telegram účtu (ne bota) přes user API (MTProto / Telethon).
Na rozdíl od bota umí napsat komukoli a unese VÍCE souběžných agentů na jednom účtu
(jako Telegram otevřený zároveň na PC, tabletu i mobilu).
⚠️ Jedná JMÉNEM přihlášeného účtu. Session soubor = plný přístup k účtu.
⚠️ Nové účty na automatizaci Telegram rychle banuje (zvlášť VoIP čísla — použij reálnou SIM).
────────────────────────────────────────────────────────────────────────
VÍCE AGENTŮ NA JEDNOM ÚČTU
────────────────────────────────────────────────────────────────────────
- api_id/api_hash se SDÍLÍ (identifikují „aplikaci", ne zařízení).
- Každý agent musí mít VLASTNÍ session soubor (= vlastní autorizace / „zařízení").
Sdílet jednu session mezi procesy NELZE (database is locked / AUTH_KEY_DUPLICATED).
→ každý agent se přihlásí zvlášť: `login --jako <jmeno>` (jeden SMS kód na agenta).
- Všechny sessions vidí stejný chat, proto se odpovědi směrují přes Telegram **Reply**:
agent pošle označenou otázku a bere jen tu odpověď, která je Reply na *jeho* zprávu
(shoda `reply_to_msg_id`). Tím se odpovědi více agentů nepomíchají.
Konfigurace v `Medevio/.env` (api_id/api_hash z https://my.telegram.org):
TELEGRAM_API_ID=1234567
TELEGRAM_API_HASH=abcdef0123456789abcdef0123456789
TELEGRAM_PHONE=+420... # nepovinné (jinak se zeptá při loginu)
Session soubory: `Medevio/agent_telegram_<jmeno>.session` (gitignored).
────────────────────────────────────────────────────────────────────────
CLI
────────────────────────────────────────────────────────────────────────
Jednorázové přihlášení agenta (spusť ve svém terminálu — čeká na kód z SMS):
python -m Knihovny.telegram_user login --jako recepty
python -m Knihovny.telegram_user login --jako kalendar
Poslání zprávy ("me" = Uložené zprávy / Saved Messages):
python -m Knihovny.telegram_user send me "Test" --jako recepty
Otázka + čekání na Reply odpověď (vypíše odpověď na stdout):
python -m Knihovny.telegram_user ask recepty "Mam pokracovat? (ano/ne)"
────────────────────────────────────────────────────────────────────────
ZE SKRIPTU
────────────────────────────────────────────────────────────────────────
from Knihovny.telegram_user import posli_jako_ja, zeptej_se_jako
posli_jako_ja("me", "Pipeline 08 hotová", session="recepty")
odp = zeptej_se_jako("recepty", "Našel jsem 3 sporné záznamy. Pokračovat?")
if odp and odp.strip().lower() == "ano":
...
"""
import argparse
import os
import sys
import time
from pathlib import Path
# telethon.sync zpřístupní metody synchronně (bez async/await)
from telethon.sync import TelegramClient
from telethon.errors import SessionPasswordNeededError, PhoneNumberUnoccupiedError
def _load_env():
env_path = Path(__file__).resolve().parent.parent / "Medevio" / ".env"
if env_path.exists():
for line in env_path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
os.environ.setdefault(k.strip(), v.strip())
_load_env()
def _api_id() -> int:
val = os.environ.get("TELEGRAM_API_ID")
if not val:
raise RuntimeError("Chybí TELEGRAM_API_ID v Medevio/.env (z https://my.telegram.org)")
return int(val)
def _api_hash() -> str:
val = os.environ.get("TELEGRAM_API_HASH")
if not val:
raise RuntimeError("Chybí TELEGRAM_API_HASH v Medevio/.env (z https://my.telegram.org)")
return val
def _session_path(jmeno: str | None) -> Path:
base = f"agent_telegram_{jmeno}" if jmeno else "agent_telegram"
return Path(__file__).resolve().parent.parent / "Medevio" / base
def _new_client(session: str | None = None) -> TelegramClient:
return TelegramClient(str(_session_path(session)), _api_id(), _api_hash())
def prihlas(session: str | None = None) -> None:
"""
Jednorázové přihlášení dané session. Interaktivně se zeptá na kód z SMS
a případně na heslo dvoufázového ověření. Vytvoří session soubor.
SPOUŠTĚJ V TERMINÁLU (potřebuje input).
"""
client = _new_client(session)
client.start(phone=os.environ.get("TELEGRAM_PHONE") or (lambda: input("Telefon (+420...): ")))
me = client.get_me()
print(f"Session '{session or 'default'}' přihlášena jako "
f"{me.first_name or ''} (@{me.username}) id={me.id}")
client.disconnect()
def _phone() -> str:
val = os.environ.get("TELEGRAM_PHONE")
if not val:
raise RuntimeError("Chybí TELEGRAM_PHONE v Medevio/.env")
return val
def login_posli_kod(session: str | None = None) -> None:
"""
1. krok přihlášení (řízeného na dálku): vyžádá si od Telegramu kód.
Vytiskne `PHONE_CODE_HASH=...`, který je potřeba pro 2. krok.
"""
client = _new_client(session)
client.connect()
try:
sent = client.send_code_request(_phone())
print("PHONE_CODE_HASH=" + sent.phone_code_hash)
finally:
client.disconnect()
def login_dokonci(code, phone_code_hash: str, session: str | None = None,
password: str | None = None) -> None:
"""
2. krok přihlášení: dokončí login zadaným kódem (a případně heslem 2FA).
Při úspěchu uloží session soubor.
"""
client = _new_client(session)
client.connect()
try:
try:
client.sign_in(phone=_phone(), code=str(code), phone_code_hash=phone_code_hash)
except SessionPasswordNeededError:
if not password:
print("NEED_PASSWORD")
return
client.sign_in(password=password)
except PhoneNumberUnoccupiedError:
print("UCET_NEEXISTUJE - nejdriv zaregistruj cislo v aplikaci Telegram")
return
me = client.get_me()
print(f"OK prihlaseno jako {me.first_name or ''} (@{me.username}) id={me.id}")
finally:
client.disconnect()
def posli_jako_ja(komu, text: str, *, session: str | None = None):
"""
Pošle zprávu jménem přihlášeného účtu z dané session.
:param komu: "me" (Saved Messages) | "@username" | telefon | int id
:param text: text zprávy
:param session: jméno session (které přihlášení použít)
:return: odeslaná zpráva (Telethon Message)
"""
with _new_client(session) as client:
if not client.is_user_authorized():
raise RuntimeError(
f"Session '{session or 'default'}' není přihlášena — "
f"spusť: python -m Knihovny.telegram_user login"
+ (f" --jako {session}" if session else "")
)
return client.send_message(komu, text)
def precti_zpravy(komu, limit: int = 10, *, session: str | None = None):
"""
Vrátí posledních `limit` zpráv z daného chatu.
:return: list dictů {"id", "text", "odeslal_ja", "reply_na", "datum"}
"""
out = []
with _new_client(session) as client:
if not client.is_user_authorized():
raise RuntimeError(f"Session '{session or 'default'}' není přihlášena.")
for msg in client.iter_messages(komu, limit=limit):
out.append({
"id": msg.id,
"text": msg.text or "",
"odeslal_ja": bool(msg.out),
"reply_na": msg.reply_to_msg_id,
"datum": msg.date,
})
return out
def zeptej_se_jako(
agent: str,
otazka: str,
*,
komu="me",
session: str | None = None,
timeout: int = 300,
poll_interval: int = 3,
vyzaduj_reply: bool = True,
) -> str | None:
"""
Pošle označenou otázku ("[agent] otázka") a BLOKUJÍCÍ čeká na odpověď.
Při více agentech naráz se odpovědi rozlišují přes Telegram **Reply**:
bere jen tu příchozí zprávu, která je Reply na právě odeslanou otázku.
:param agent: jméno agenta (objeví se v textu otázky jako štítek)
:param otazka: text otázky
:param komu: kam poslat ("me" = Saved Messages | "@username" | id)
:param session: jméno session; výchozí = `agent` (každý agent svůj soubor)
:param timeout: celkové čekání v sekundách (pak vrátí None)
:param poll_interval: jak často kontrolovat nové zprávy (s)
:param vyzaduj_reply: True = bere jen Reply na svou otázku (bezpečné pro víc agentů);
False = vezme první příchozí zprávu (jen pro 1 agenta)
:return: text odpovědi, nebo None při timeoutu
"""
session = session or agent
with _new_client(session) as client:
if not client.is_user_authorized():
raise RuntimeError(
f"Session '{session}' není přihlášena — "
f"spusť: python -m Knihovny.telegram_user login --jako {session}"
)
sent = client.send_message(komu, f"[{agent}] {otazka}")
qid = sent.id
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
# jen zprávy novější než naše otázka, od nejstarší
for msg in client.iter_messages(komu, min_id=qid, reverse=True):
if msg.out:
continue # naše vlastní zpráva
if vyzaduj_reply:
if msg.reply_to_msg_id == qid:
return msg.text
else:
return msg.text
zbyva = deadline - time.monotonic()
if zbyva <= 0:
break
time.sleep(min(poll_interval, max(1, zbyva)))
return None
def _safe_print(text: str):
try:
print(text)
except UnicodeEncodeError:
print(text.encode("ascii", "replace").decode("ascii"))
def _main():
try:
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
parser = argparse.ArgumentParser(prog="telegram_user", description="Telegram user účet (Telethon)")
sub = parser.add_subparsers(dest="cmd", required=True)
p_login = sub.add_parser("login", help="jednorázové přihlášení session")
p_login.add_argument("--jako", dest="jako", default=None, help="jméno session/agenta")
p_send = sub.add_parser("send", help="poslat zprávu")
p_send.add_argument("komu", help='"me" | "@username" | telefon | id')
p_send.add_argument("text", help="text zprávy")
p_send.add_argument("--jako", dest="jako", default=None, help="jméno session")
p_ask = sub.add_parser("ask", help="poslat otázku a počkat na Reply odpověď")
p_ask.add_argument("agent", help="jméno agenta (štítek + výchozí session)")
p_ask.add_argument("text", help="text otázky")
p_ask.add_argument("--komu", dest="komu", default="me", help='kam (výchozí "me")')
p_ask.add_argument("--timeout", dest="timeout", type=int, default=240, help="čekání v s")
args = parser.parse_args()
if args.cmd == "login":
prihlas(args.jako)
elif args.cmd == "send":
posli_jako_ja(args.komu, args.text, session=args.jako)
_safe_print("Odesláno OK")
elif args.cmd == "ask":
odp = zeptej_se_jako(args.agent, args.text, komu=args.komu, timeout=args.timeout)
if odp is None:
_safe_print("(bez odpovědi — vypršel timeout)")
sys.exit(2)
_safe_print(odp)
if __name__ == "__main__":
_main()