237 lines
10 KiB
Python
237 lines
10 KiB
Python
import asyncio
|
||
import os
|
||
import sys
|
||
import logging
|
||
import random
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from playwright.async_api import async_playwright
|
||
|
||
# Setup logging
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format='%(asctime)s [%(levelname)s] %(message)s',
|
||
handlers=[
|
||
logging.FileHandler("/app/logs/sudrf.log", encoding="utf-8"),
|
||
logging.StreamHandler(sys.stdout)
|
||
]
|
||
)
|
||
logger = logging.getLogger("SUD-Reaper")
|
||
|
||
USER_DATA_DIR = "/app/browser_context_sudrf"
|
||
EXPORT_DIR = "/app/exports/sudrf"
|
||
os.makedirs(EXPORT_DIR, exist_ok=True)
|
||
os.makedirs("/app/logs", exist_ok=True)
|
||
|
||
BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "8725618164:AAH1tGalq-pw1l0t4P5c0sdLkCVJdh7IE7M")
|
||
CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "197957361")
|
||
|
||
def send_tg(msg):
|
||
import subprocess
|
||
subprocess.run(["curl", "-s", "-X", "POST", f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",
|
||
"-d", f"chat_id={CHAT_ID}&text={msg}&parse_mode=markdown"])
|
||
|
||
async def human_delay(min_sec=3, max_sec=6):
|
||
await asyncio.sleep(random.uniform(min_sec, max_sec))
|
||
|
||
async def run_mission():
|
||
logger.info("Starting Playwright in VNC display...")
|
||
send_tg("🕵️♂️ **ГАС Правосудие: Запуск робота-разведчика**\nОжидаю авторизацию через Госуслуги во VNC: http://192.168.10.116:6080/vnc.html")
|
||
|
||
async with async_playwright() as p:
|
||
# Launch persistent browser context
|
||
context = await p.chromium.launch_persistent_context(
|
||
user_data_dir=USER_DATA_DIR,
|
||
headless=False,
|
||
args=["--no-sandbox", "--disable-setuid-sandbox", "--start-maximized"]
|
||
)
|
||
|
||
page = context.pages[0] if context.pages else await context.new_page()
|
||
await page.set_viewport_size({"width": 1280, "height": 1024})
|
||
|
||
# Navigate to portal
|
||
logger.info("Navigating to sudrf...")
|
||
await page.goto("https://ej.sudrf.ru/")
|
||
await asyncio.sleep(8)
|
||
|
||
# Check login screen
|
||
url = page.url
|
||
try:
|
||
visible_text = await page.locator("body").inner_text()
|
||
except Exception:
|
||
visible_text = ""
|
||
|
||
logger.info(f"Current page URL: {url}")
|
||
|
||
is_logged_in = "sudrf.ru" in url and any(ind in visible_text for ind in ["Выйти", "Выход", "Личный кабинет", "Тырин"]) and "Авторизация" not in visible_text
|
||
|
||
if not is_logged_in:
|
||
logger.info("Login/Agreement page detected. Auto-accepting terms...")
|
||
try:
|
||
checkbox = await page.query_selector("input[type='checkbox']")
|
||
if checkbox:
|
||
await checkbox.check()
|
||
logger.info("Checked user agreement checkbox.")
|
||
else:
|
||
await page.click("text=Я ознакомился(ась)", timeout=5000)
|
||
logger.info("Clicked user agreement label.")
|
||
await asyncio.sleep(2)
|
||
|
||
buttons = await page.query_selector_all("button, .btn, a.btn")
|
||
for b in buttons:
|
||
text = (await b.inner_text()).strip()
|
||
if "Войти" in text:
|
||
logger.info(f"Clicking Войти button: '{text}'")
|
||
await b.click()
|
||
break
|
||
except Exception as e:
|
||
logger.info(f"Note (agreement handling): {e}")
|
||
|
||
logger.info("Waiting up to 300s for user authorization...")
|
||
logged_in_False = True
|
||
for i in range(60):
|
||
await asyncio.sleep(5)
|
||
current_url = page.url
|
||
try:
|
||
current_text = await page.locator("body").inner_text()
|
||
except Exception:
|
||
current_text = ""
|
||
|
||
if "sudrf.ru" in current_url and any(ind in current_text for ind in ["Выйти", "Выход", "Личный кабинет", "Тырин"]):
|
||
logger.info(f"Login detected! URL: {current_url}")
|
||
logged_in_False = False
|
||
break
|
||
if i % 6 == 0:
|
||
logger.info(f"Still waiting... {i*5}s")
|
||
if logged_in_False:
|
||
logger.error("Login timeout exceeded.")
|
||
send_tg("❌ **ГАС Правосудие: Превышено время ожидания входа**.")
|
||
await context.close()
|
||
return
|
||
else:
|
||
logger.info("Already logged in.")
|
||
|
||
# Navigate to appeals history page
|
||
logger.info("Navigating to appeals history...")
|
||
send_tg("✅ **Авторизация подтверждена!** Загружаю историю обращений...")
|
||
await page.goto("https://ej.sudrf.ru/appeals/")
|
||
await asyncio.sleep(8)
|
||
|
||
# Click search button
|
||
logger.info("Locating Search button...")
|
||
buttons = await page.query_selector_all("button, input[type='submit'], input[type='button'], .btn, a.btn")
|
||
search_clicked = False
|
||
for b in buttons:
|
||
try:
|
||
text = (await b.inner_text()).strip()
|
||
if "Найти" in text:
|
||
logger.info(f"Clicking Search button: '{text}'")
|
||
await b.click(force=True)
|
||
search_clicked = True
|
||
break
|
||
except Exception as e:
|
||
logger.error(f"Error inspecting search button: {e}")
|
||
|
||
if not search_clicked:
|
||
logger.error("Could not click Search button.")
|
||
send_tg("❌ **Ошибка**: Не удалось нажать кнопку Поиск для загрузки списка обращений.")
|
||
await page.screenshot(path=os.path.join(EXPORT_DIR, "search_failed.png"), full_page=True)
|
||
await context.close()
|
||
return
|
||
|
||
logger.info("Waiting for search results...")
|
||
await asyncio.sleep(8)
|
||
|
||
# Check if table loaded
|
||
tbodies = await page.query_selector_all("table.table-history tbody")
|
||
logger.info(f"Found {len(tbodies)} tbodies in history table.")
|
||
|
||
if not tbodies:
|
||
logger.warning("No tbodies found in history table.")
|
||
send_tg("⚠️ **Предупреждение**: История обращений пуста или не загрузилась.")
|
||
await page.screenshot(path=os.path.join(EXPORT_DIR, "empty_table.png"), full_page=True)
|
||
await context.close()
|
||
return
|
||
|
||
# Process each appeal / tbody
|
||
count = 0
|
||
for index, tbody in enumerate(tbodies[:15]): # Process top 15 appeals
|
||
main_tr = await tbody.query_selector("tr:not(.tr-expanded)")
|
||
if not main_tr:
|
||
continue
|
||
|
||
cells = await main_tr.query_selector_all("td")
|
||
if len(cells) < 5:
|
||
continue
|
||
|
||
# Get registration number details
|
||
num_text = (await cells[1].inner_text()).strip()
|
||
# Clean registration number to get safe directory ID
|
||
safe_id = "".join([c for c in num_text if c.isalnum() or c in ("-", "_")]).strip()
|
||
if not safe_id:
|
||
safe_id = f"row_{index}"
|
||
|
||
appeal_dir = os.path.join(EXPORT_DIR, f"appeal_{safe_id}")
|
||
os.makedirs(appeal_dir, exist_ok=True)
|
||
|
||
# Find and click "История рассмотрения" link in 5th cell
|
||
history_link = await cells[4].query_selector("a:has-text('История рассмотрения')")
|
||
if not history_link:
|
||
logger.info(f"No history link for row {index}, safe_id={safe_id}")
|
||
continue
|
||
|
||
logger.info(f"Processing appeal {safe_id} (row {index})...")
|
||
|
||
try:
|
||
# Expand
|
||
await history_link.click(force=True)
|
||
await asyncio.sleep(4)
|
||
|
||
# Take screenshot of expanded tbody segment
|
||
evidence_img = os.path.join(appeal_dir, "evidence.png")
|
||
await tbody.screenshot(path=evidence_img)
|
||
|
||
# Save tbody HTML
|
||
detail_html = os.path.join(appeal_dir, "page.html")
|
||
with open(detail_html, "w", encoding="utf-8") as f:
|
||
f.write(await tbody.inner_html())
|
||
|
||
# Download attachments
|
||
file_links = await tbody.query_selector_all("a[href*='getFile']")
|
||
files_downloaded = []
|
||
|
||
for file_link in file_links:
|
||
file_text = (await file_link.inner_text()).strip()
|
||
logger.info(f"Downloading file: '{file_text}'")
|
||
|
||
try:
|
||
async with page.expect_download(timeout=15000) as download_info:
|
||
await file_link.click()
|
||
download = await download_info.value
|
||
dest = os.path.join(appeal_dir, download.suggested_filename)
|
||
await download.save_as(dest)
|
||
files_downloaded.append(download.suggested_filename)
|
||
logger.info(f"Saved: {download.suggested_filename}")
|
||
except Exception as e:
|
||
logger.warning(f"Error downloading file {file_text}: {e}")
|
||
|
||
count += 1
|
||
send_tg(f"📥 **Обращение {count} скачано: № {safe_id}**\nФайлов: {len(files_downloaded)}")
|
||
|
||
# Collapse
|
||
await history_link.click(force=True)
|
||
await asyncio.sleep(2)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error parsing appeal {safe_id}: {e}")
|
||
|
||
send_tg(f"🏁 **Жатва ГАС Правосудие завершена!** Собрано обращений: {count}. Запускаю сборку в PDF.")
|
||
|
||
# Trigger compiler
|
||
import subprocess
|
||
subprocess.Popen(["python3", "/app/sudrf_pdf_compiler.py"], start_new_session=True)
|
||
|
||
await context.close()
|
||
|
||
if __name__ == "__main__":
|
||
asyncio.run(run_mission())
|