diff --git a/email_stats_2026.png b/email_stats_2026.png new file mode 100644 index 0000000..3c3c8bd Binary files /dev/null and b/email_stats_2026.png differ diff --git a/fetch_and_build_remaining_laws.py b/fetch_and_build_remaining_laws.py new file mode 100755 index 0000000..70dac3f --- /dev/null +++ b/fetch_and_build_remaining_laws.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +""" +Скрипт автономной сборки официальных полных текстов действующих нормативно-правовых актов РФ +с портала КонсультантПлюс в структурированные PDF через wkhtmltopdf. + +Собираемые документы: +1. Постановление Правительства РФ № 354 (cons_doc_LAW_114247) +2. Постановление Госстроя РФ № 170 (cons_doc_LAW_44772) +3. СанПиН 2.1.3684-21 (cons_doc_LAW_376166) +4. Приказ Генеральной прокуратуры РФ № 45 (cons_doc_LAW_141740) +""" + +import os +import sys +import time +import subprocess +import urllib.request +from bs4 import BeautifulSoup +import fitz + +HEADERS = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'} + +DOCS = [ + { + 'law_id': 'cons_doc_LAW_114247', + 'out_name': '04_ПП_РФ_354_Коммунальные_Услуги_Полный.pdf', + 'title_prefix': 'Постановление Правительства РФ от 06.05.2011 № 354' + }, + { + 'law_id': 'cons_doc_LAW_44772', + 'out_name': '06_Постановление_Госстроя_170_Эксплуатация_Жилфонда_Полный.pdf', + 'title_prefix': 'Постановление Госстроя РФ от 27.09.2003 № 170 (МДК 2-03.2003)' + }, + { + 'law_id': 'cons_doc_LAW_376166', + 'out_name': '07_СанПиН_2.1.3684-21_Санитарные_Требования_МКД_Полный.pdf', + 'title_prefix': 'СанПиН 2.1.3684-21 (Постановление Главного санврача РФ № 3)' + }, + { + 'law_id': 'cons_doc_LAW_141740', + 'out_name': '08_Приказ_Генпрокуратуры_45_Инструкция_Рассмотрения_Обращений_Полный.pdf', + 'title_prefix': 'Приказ Генеральной прокуратуры РФ от 30.01.2013 № 45' + } +] + +DEST_DIR = '/home/matrixhasyou/mutt/legal_sources' +os.makedirs(DEST_DIR, exist_ok=True) + +def build_law(law_info): + law_id = law_info['law_id'] + out_pdf = os.path.join(DEST_DIR, law_info['out_name']) + base_url = f'https://www.consultant.ru/document/{law_id}/' + + print(f"\n=======================================================") + print(f"[*] Сборка документа: {law_info['title_prefix']} ({law_id})") + print(f"=======================================================") + + req = urllib.request.Request(base_url, headers=HEADERS) + try: + with urllib.request.urlopen(req, timeout=15) as resp: + soup = BeautifulSoup(resp.read().decode('utf-8', errors='ignore'), 'html.parser') + except Exception as e: + print(f"[-] Ошибка загрузки оглавления {base_url}: {e}") + return False + + raw_title = soup.find('title').text + main_title = raw_title.split('\\')[0].strip() if '\\' in raw_title else raw_title.strip() + + links = [] + seen = set() + for a in soup.find_all('a', href=True): + href = a['href'] + if href.startswith(f'/document/{law_id}/') and len(href) > len(f'/document/{law_id}/'): + clean_href = href.split('#')[0] + if clean_href not in seen: + seen.add(clean_href) + links.append((clean_href, a.text.strip())) + + print(f"[+] Найдено разделов/статей: {len(links)}") + + html_parts = [f""" + + + + + + +

{main_title}

+
Официальный действующий текст в актуальной редакции со всеми изменениями и приложениями
+"""] + + for i, (href, sec_title) in enumerate(links, 1): + url = 'https://www.consultant.ru' + href + print(f" [{i}/{len(links)}] Загрузка: {sec_title[:60]}...") + r = urllib.request.Request(url, headers=HEADERS) + try: + with urllib.request.urlopen(r, timeout=20) as res: + sec_soup = BeautifulSoup(res.read().decode('utf-8', errors='ignore'), 'html.parser') + html_parts.append(f'

{sec_title}

') + for elem in sec_soup.find_all(['p', 'table']): + if elem.name == 'p': + t = elem.text.strip() + if t and not 'Подготовлена редакция' in t and not 'КонсультантПлюс' in t and not 'Открыть документ' in t: + html_parts.append(f'

{t}

') + elif elem.name == 'table': + html_parts.append(str(elem)) + time.sleep(0.2) + except Exception as e: + print(f" [!] Ошибка загрузки раздела {href}: {e}") + + html_parts.append('') + + tmp_html = f'/tmp/{law_id}.html' + with open(tmp_html, 'w', encoding='utf-8') as f: + f.write('\n'.join(html_parts)) + + print(f"[*] Компиляция PDF через wkhtmltopdf: {out_pdf}...") + subprocess.run([ + 'wkhtmltopdf', + '--encoding', 'utf-8', + '--margin-top', '20mm', + '--margin-bottom', '20mm', + '--margin-left', '15mm', + '--margin-right', '15mm', + tmp_html, out_pdf + ], check=True) + + try: + doc = fitz.open(out_pdf) + pages_count = len(doc) + except Exception: + pages_count = 0 + + size_kb = os.path.getsize(out_pdf) / 1024 + print(f"[✔] УСПЕШНО СОЗДАН: {out_pdf} ({size_kb:.1f} КБ, {pages_count} страниц)") + return True + +def main(): + for doc_info in DOCS: + build_law(doc_info) + print("\n[🎉] ВСЕ ЗАКОНЫ РФ ПОЛНОСТЬЮ СОБРАНЫ В PDF!") + +if __name__ == '__main__': + main() diff --git a/generate_email_stats_2026.py b/generate_email_stats_2026.py new file mode 100644 index 0000000..23a4fa3 --- /dev/null +++ b/generate_email_stats_2026.py @@ -0,0 +1,507 @@ +#!/usr/bin/env python3 +import os +import sys +import re +import fitz # PyMuPDF +from datetime import date, datetime +from pathlib import Path +import requests +from weasyprint import HTML + +# Telegram settings +BOT_TOKEN = "8725618164:AAH1tGalq-pw1l0t4P5c0sdLkCVJdh7IE7M" +ADMIN_USER_ID = "197957361" + +# Domain mapping for human-readable naming +DOMAINS_MAP = { + "gosuslugi.ru": "Госуслуги РФ", + "dom.gosuslugi.ru": "ГИС ЖКХ", + "sudrf.ru": "Судебная система РФ", + "zavolgskiy.uln@sudrf.ru": "Заволжский районный суд", + "ulgss.ru": "УО Жилстройсервис", + "ulgkh.ru": "Департамент ЖКХ", + "fssp.gov.ru": "ФССП России", + "fssprus.ru": "ФССП России", + "gov.ru": "Правительство РФ", + "ulgov.ru": "Администрация Ульяновской обл.", + "russianpost.ru": "Почта России", + "rkn.gov.ru": "Роскомнадзор", + "nalog.gov.ru": "ФНС России", + "nalog.ru": "ФНС России", + "genproc.gov.ru": "Прокуратура РФ", + "epp.genproc.gov.ru": "Прокуратура РФ", + "mvd.gov.ru": "МВД России", + "mvd.ru": "МВД России", + "rospotrebnadzor.ru": "Роспотребнадзор", + "pfr.gov.ru": "ПФР / СФР России", + "pfrf.ru": "ПФР / СФР России", + "fss.ru": "Фонд социального страхования", + "sledcom.ru": "Следственный комитет РФ", + "vkks.ru": "ВККС РФ", + "mailop.ru": "Почтовый шлюз Mailop", + "73.mailop.ru": "Почтовый шлюз 73.Mailop", + "uk-service.ru": "УК Сервис-М", + "dgi.ru": "Департамент гос. имущества", + "mos.ru": "Портал Москвы mos.ru", + "fsin.gov.ru": "ФСИН России", + "skrf.ru": "Следственный комитет РФ", + "ugpr.ru": "Управление госзакупок", + "fas.gov.ru": "ФАС России", + "minstroyrf.gov.ru": "Минстрой РФ", + "gkh.ru": "Портал ЖКХ", + "msud.ru": "Мировые судьи РФ", + "arbitr.ru": "Арбитражные суды РФ", + "mirsud": "Мировые судьи", + "msud73.ru": "Мировые судьи Ульяновской обл.", + "mirsud73.ru": "Мировые судьи Ульяновской обл.", + "court.ru": "Судебный департамент РФ", + "sud.ru": "Судебный департамент РФ", + "uloblsud.ru": "Ульяновский областной суд", + "duma.gov.ru": "Государственная Дума РФ", +} + +def send_telegram_photo(photo_bytes, caption): + url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendPhoto" + files = { + "photo": ("stats.png", photo_bytes, "image/png") + } + data = { + "chat_id": ADMIN_USER_ID, + "caption": caption, + "parse_mode": "HTML" + } + + print("Отправка отчета в Telegram...") + try: + r = requests.post(url, data=data, files=files, timeout=30) + if r.status_code == 200: + print("🎉 Картинка со статистикой успешно отправлена!") + else: + print(f"❌ Ошибка отправки: {r.status_code} - {r.text}") + except Exception as e: + print(f"❌ Ошибка отправки запроса в Telegram: {e}") + +def main(): + folder = Path("/home/matrixhasyou/mutt/exported_emails/all") + if not folder.exists(): + print(f"Папка не существует: {folder}") + sys.exit(1) + + pdf_files = list(folder.glob("*.pdf")) + print(f"Всего PDF файлов найдено: {len(pdf_files)}") + + stats = {} + + # Summary counters + total_in_mails = 0 + total_in_pages = 0 + total_in_atts = 0 + total_out_mails = 0 + total_out_pages = 0 + total_out_atts = 0 + + # Process files + for filepath in pdf_files: + filename = filepath.name + + # Parse filename + # Format is typically: {match_domain}_{date_part}_{safe_from}_{safe_subject}.pdf + # Let's extract date + date_match = re.search(r'_(\d{4}-\d{2}-\d{2})_', filename) + if not date_match: + # Maybe it starts with date: YYYY-MM-DD_domain_... + date_match = re.match(r'^(\d{4}-\d{2}-\d{2})_', filename) + + if not date_match: + continue + + date_str = date_match.group(1) + if not date_str.startswith("2026"): + continue + + # Parse domain and sender + parts = filename.split("_", 2) + if len(parts) >= 2: + # If domain first + domain = parts[0] + # Verify if it looks like domain + if not ("." in domain or "судебное" in domain or "other" in domain): + # Fallback to general parsing + domain = "other" + else: + domain = "other" + + try: + doc = fitz.open(filepath) + num_pages = len(doc) + first_page_text = doc[0].get_text() + + # Determine direction (inbound / outbound) + is_outbound = False + first_page_lower = first_page_text.lower() + + # Heuristics + if "от: ktybdsq@gmail.com" in first_page_lower or "from: ktybdsq@gmail.com" in first_page_lower: + is_outbound = True + elif "_ktybdsq_" in filename.lower(): + is_outbound = True + elif "fwd_" in filename.lower() and "от: ktybdsq@gmail.com" in first_page_lower: + is_outbound = True + + # Attachment count + att_count = 0 + att_match = re.search(r"(?:Вложения|Attachments):(.*)", first_page_text, re.DOTALL | re.IGNORECASE) + if att_match: + block_text = att_match.group(1) + # Count bytes occurrences as an indicator of attachments + att_count = len(re.findall(r"bytes\)", block_text)) + + doc.close() + + # Add to stats + if domain not in stats: + stats[domain] = { + 'inbound': {'mails': 0, 'pages': 0, 'atts': 0}, + 'outbound': {'mails': 0, 'pages': 0, 'atts': 0} + } + + direction = 'outbound' if is_outbound else 'inbound' + stats[domain][direction]['mails'] += 1 + stats[domain][direction]['pages'] += num_pages + stats[domain][direction]['atts'] += att_count + + if is_outbound: + total_out_mails += 1 + total_out_pages += num_pages + total_out_atts += att_count + else: + total_in_mails += 1 + total_in_pages += num_pages + total_in_atts += att_count + + except Exception as e: + print(f"Ошибка парсинга {filename}: {e}") + + # Group results to consolidate subdomains or same entities + grouped_stats = {} + for dom, data in stats.items(): + mapped_name = DOMAINS_MAP.get(dom, dom) + + # General groupings for cleaner output + if "sudrf.ru" in dom or "mirsud" in dom or "msud" in dom or "court" in dom or "arbitr" in dom: + mapped_name = "Судебная система РФ" + elif "gosuslugi.ru" in dom: + mapped_name = "Портал Госуслуг РФ" + elif "gov.ru" in dom or "ulgov.ru" in dom: + mapped_name = "Органы власти / Ведомства" + elif "fssp.gov.ru" in dom or "fssprus.ru" in dom: + mapped_name = "ФССП (Судебные приставы)" + elif "nalog.ru" in dom or "nalog.gov.ru" in dom: + mapped_name = "ФНС (Налоговая служба)" + + if mapped_name not in grouped_stats: + grouped_stats[mapped_name] = { + 'inbound': {'mails': 0, 'pages': 0, 'atts': 0}, + 'outbound': {'mails': 0, 'pages': 0, 'atts': 0} + } + + for dir_key in ['inbound', 'outbound']: + for val_key in ['mails', 'pages', 'atts']: + grouped_stats[mapped_name][dir_key][val_key] += data[dir_key][val_key] + + # Sort domains by total mail activity + sorted_domains = sorted( + grouped_stats.keys(), + key=lambda k: grouped_stats[k]['inbound']['mails'] + grouped_stats[k]['outbound']['mails'], + reverse=True + ) + + # Render HTML to PDF and PNG + html_content = """ + + + + + + +
+
+
+

⚖️ ОФИЦИАЛЬНАЯ ПЕРЕПИСКА С ВЕДОМСТВАМИ (2026 Г.)

+
Анализ входящих и исходящих официальных документов и страниц
+
+
+ Период: 01.01.2026 - """ + date.today().strftime('%d.%m.%Y') + """ +
+
+ +
+
+ + + + + + + + + + + + + + + + + + """ + + for dom in sorted_domains: + in_m = grouped_stats[dom]['inbound']['mails'] + in_a = grouped_stats[dom]['inbound']['atts'] + in_p = grouped_stats[dom]['inbound']['pages'] + + out_m = grouped_stats[dom]['outbound']['mails'] + out_a = grouped_stats[dom]['outbound']['atts'] + out_p = grouped_stats[dom]['outbound']['pages'] + + if in_m == 0 and out_m == 0: + continue + + html_content += f""" + + + + + + + + + + """ + + html_content += f""" + + + + + + + + + + +
Ведомство / Организация📥 Входящие (мне)📤 Исходящие (от меня)
ПисемВлож.Стр.ПисемВлож.Стр.
{dom}{in_m if in_m > 0 else '-'}{in_a if in_a > 0 else '-'}{in_p if in_p > 0 else '-'}{out_m if out_m > 0 else '-'}{out_a if out_a > 0 else '-'}{out_p if out_p > 0 else '-'}
ИТОГО:{total_in_mails}{total_in_atts}{total_in_pages}{total_out_mails}{total_out_atts}{total_out_pages}
+
+ +
+
+
📊 Сводные данные
+ +
+
Всего писем
+
{total_in_mails + total_out_mails}
+
+ +
+
Всего файлов / вложений
+
{total_in_atts + total_out_atts} шт.
+
+ +
+
Общий объем документов
+
{total_in_pages + total_out_pages} стр.
+
+ +
+
Входящий поток
+
{total_in_mails} писем / {total_in_pages} стр.
+
+ +
+
Исходящий поток
+
{total_out_mails} писем / {total_out_pages} стр.
+
+
+ + +
+
+
+ + + """ + + # Render HTML to PDF using WeasyPrint + print("Рендеринг HTML отчета в PDF...") + pdf_bytes = HTML(string=html_content).write_pdf() + + # Convert PDF to PNG + print("Конвертация PDF в PNG...") + doc = fitz.open(stream=pdf_bytes, filetype="pdf") + page = doc[0] + pix = page.get_pixmap(dpi=180) + png_bytes = pix.tobytes("png") + doc.close() + + # Save the output PNG image + out_png_path = Path("/home/matrixhasyou/mutt/email_stats_2026.png") + out_png_path.write_bytes(png_bytes) + print(f"✅ Итоговый график сохранен: {out_png_path}") + + # Send to Telegram + caption = ( + f"📊 Статистика переписки с ведомствами за весь 2026 год\n\n" + f"🔹 Всего писем: {total_in_mails + total_out_mails}\n" + f"📥 Входящих: {total_in_mails} (вложений: {total_in_atts}, страниц: {total_in_pages})\n" + f"📤 Исходящих: {total_out_mails} (вложений: {total_out_atts}, страниц: {total_out_pages})\n" + f"📁 Общий объем архива: {total_in_pages + total_out_pages} страниц" + ) + send_telegram_photo(png_bytes, caption) + +if __name__ == "__main__": + main() diff --git a/last_ai_sync.txt b/last_ai_sync.txt index ce8f117..c57774e 100644 --- a/last_ai_sync.txt +++ b/last_ai_sync.txt @@ -1 +1 @@ -2026-06-26 21:41:52 \ No newline at end of file +2026-10-13 02:00:34 \ No newline at end of file diff --git a/pipeline_status.json b/pipeline_status.json index e143efa..ded4ff1 100644 --- a/pipeline_status.json +++ b/pipeline_status.json @@ -1 +1 @@ -{"status": "success", "step": "\u0421\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0430\u0446\u0438\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", "updated_at": "2026-06-26 21:41:52", "new_emails": 1, "total_emails": 0, "pdf_size_mb": 180.84} \ No newline at end of file +{"status": "success", "step": "\u0421\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0430\u0446\u0438\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", "updated_at": "2026-10-13 02:00:35", "new_emails": 2, "total_emails": 612, "pdf_size_mb": 203.87} \ No newline at end of file