303 lines
12 KiB
Python
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()
|