FIX: Полное восстановление веб-панели, фикс экспорта JSON, исправление путей PATH в системных юнитах и обновление шаблонов

This commit is contained in:
Admin 2026-04-18 23:22:41 +04:00
parent 434cd71505
commit d068e8137f
26 changed files with 8861 additions and 484 deletions

1
.gitignore vendored
View file

@ -31,7 +31,6 @@ logs/
*.log
# Exports
exports/
backups/
# OS

25
EXPORTS_FIX_LOG.md Normal file
View file

@ -0,0 +1,25 @@
# 📜 История исправлений REBORN (LKM37) - ЭКСПОРТ И ПРОЦЕССЫ
## 18 апреля 2026 г. (Финал)
### 🛑 Что было сломано:
1. **Экспорт JSON**: Старая логика была завязана на 7 дней, а ручной экспорт падал из-за отсутствия моделей и таймаутов Git Push (объем данных > 260Мб).
2. **Дубликаты процессов**: Бот сам запускал веб-сервер внутри `on_startup`, что приводило к конфликтам порта 8000 при перезапусках.
3. **Git/Gitea**: `exports/` был в `.gitignore`, что блокировало автоматическую отправку. Прокси мешал локальному доступу к Synology.
### ✅ Что сделано (РЕЗУЛЬТАТ):
1. **Новый сервис `ChatExporter`**:
- Полный экспорт всей истории без лимитов.
- Прямой Push в Gitea (использует `git add -f`).
- Учет всех выгрузок в новой таблице `chat_exports`.
2. **Разделение сервисов**:
- `domovoy-bot.service`: Только Telegram-бот и планировщик.
- `domovoy-web.service`: Отдельный сервис для админки (порт 8000).
- Это исключило конфликты портов раз и навсегда.
3. **Исправление путей и времени**: Везде используется Ульяновск (UTC+4) и динамические пути проекта.
### 💡 Как пользоваться:
- **Кнопка в меню**: Собирает всё и пушит в Гит. Если файл большой — подождите 30 секунд.
- **Статистика**: На странице экспорта теперь видно общее количество выгрузок и дату последней.
- **Автоматика**: Планировщик раз в неделю делает то же самое.
**ВАЖНО**: Для работы с Gitea прокси не нужен. Скрипт сам сбрасывает переменные окружения при пуше.

View file

@ -0,0 +1,20 @@
import sqlite3
import os
db_path = "database/domovoy.db"
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS chat_exports (
id INTEGER PRIMARY KEY AUTOINCREMENT,
export_date DATETIME DEFAULT CURRENT_TIMESTAMP,
filename TEXT NOT NULL,
total_messages INTEGER,
git_pushed BOOLEAN DEFAULT 0
)
''')
conn.commit()
conn.close()
print("✅ Таблица chat_exports создана")

View file

@ -0,0 +1,40 @@
{
"export_date": "2026-02-28T12:47:20.562331",
"chat_id": null,
"period": {
"from": "2026-02-21T12:47:20.558312",
"to": "2026-02-28T12:47:20.562352"
},
"summary": {
"total_users": 2,
"verified_users": 2,
"total_messages": 0
},
"users": [
{
"user_id": 5475262081,
"username": null,
"first_name": "Надежда",
"last_name": "Тырина",
"apartment": "152",
"verified": true,
"join_date": "2026-02-23T16:46:55.844916",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 197957361,
"username": "ktybdsq",
"first_name": "Александр",
"last_name": "MatrixHasYou",
"apartment": "152",
"verified": true,
"join_date": "2026-02-23T13:28:08.090899",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
}
],
"messages": []
}

View file

@ -0,0 +1,40 @@
{
"export_date": "2026-03-02T03:00:00.039339",
"chat_id": null,
"period": {
"from": "2026-02-23T03:00:00.034574",
"to": "2026-03-02T03:00:00.039354"
},
"summary": {
"total_users": 2,
"verified_users": 2,
"total_messages": 0
},
"users": [
{
"user_id": 5475262081,
"username": null,
"first_name": "Надежда",
"last_name": "Тырина",
"apartment": "152",
"verified": true,
"join_date": "2026-02-23T16:46:55.844916",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 197957361,
"username": "ktybdsq",
"first_name": "Александр",
"last_name": "MatrixHasYou",
"apartment": "152",
"verified": true,
"join_date": "2026-02-23T13:28:08.090899",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
}
],
"messages": []
}

View file

@ -0,0 +1,100 @@
{
"export_date": "2026-03-06T19:20:22.868011",
"chat_id": null,
"period": {
"from": "2026-02-27T19:20:22.864014",
"to": "2026-03-06T19:20:22.868033"
},
"summary": {
"total_users": 7,
"verified_users": 7,
"total_messages": 0
},
"users": [
{
"user_id": 1299551170,
"username": "katt_rin9",
"first_name": "Екатерина🐈",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-05T03:59:57.232963",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1335945859,
"username": null,
"first_name": "O4aPoBaHHblu_cTpaHHuK",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-05T03:59:57.231023",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1867403697,
"username": "cvetok_nura",
"first_name": "Светлана",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-05T03:59:57.228965",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 8181748817,
"username": null,
"first_name": "Фаина",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-05T03:59:57.226876",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 8725618164,
"username": "lkmthreeseven_bot",
"first_name": "DomovoyKontrolBot",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-05T03:59:57.221608",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 5475262081,
"username": null,
"first_name": "Надежда",
"last_name": "Тырина",
"apartment": "152",
"verified": true,
"join_date": "2026-02-23T16:46:55.844916",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 197957361,
"username": "ktybdsq",
"first_name": "Александр",
"last_name": "MatrixHasYou",
"apartment": "152",
"verified": true,
"join_date": "2026-02-23T13:28:08.090899",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
}
],
"messages": []
}

View file

@ -0,0 +1,100 @@
{
"export_date": "2026-03-06T19:20:32.719080",
"chat_id": null,
"period": {
"from": "2026-02-27T19:20:32.717289",
"to": "2026-03-06T19:20:32.719100"
},
"summary": {
"total_users": 7,
"verified_users": 7,
"total_messages": 0
},
"users": [
{
"user_id": 1299551170,
"username": "katt_rin9",
"first_name": "Екатерина🐈",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-05T03:59:57.232963",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1335945859,
"username": null,
"first_name": "O4aPoBaHHblu_cTpaHHuK",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-05T03:59:57.231023",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1867403697,
"username": "cvetok_nura",
"first_name": "Светлана",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-05T03:59:57.228965",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 8181748817,
"username": null,
"first_name": "Фаина",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-05T03:59:57.226876",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 8725618164,
"username": "lkmthreeseven_bot",
"first_name": "DomovoyKontrolBot",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-05T03:59:57.221608",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 5475262081,
"username": null,
"first_name": "Надежда",
"last_name": "Тырина",
"apartment": "152",
"verified": true,
"join_date": "2026-02-23T16:46:55.844916",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 197957361,
"username": "ktybdsq",
"first_name": "Александр",
"last_name": "MatrixHasYou",
"apartment": "152",
"verified": true,
"join_date": "2026-02-23T13:28:08.090899",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
}
],
"messages": []
}

View file

@ -0,0 +1,100 @@
{
"export_date": "2026-03-09T03:00:00.032102",
"chat_id": null,
"period": {
"from": "2026-03-02T03:00:00.028836",
"to": "2026-03-09T03:00:00.032120"
},
"summary": {
"total_users": 7,
"verified_users": 7,
"total_messages": 0
},
"users": [
{
"user_id": 1299551170,
"username": "katt_rin9",
"first_name": "Екатерина🐈",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-05T03:59:57.232963",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1335945859,
"username": null,
"first_name": "O4aPoBaHHblu_cTpaHHuK",
"last_name": null,
"apartment": "",
"verified": true,
"join_date": "2026-03-05T03:59:57.231023",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1867403697,
"username": "cvetok_nura",
"first_name": "Светлана",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-05T03:59:57.228965",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 8181748817,
"username": null,
"first_name": "Фаина",
"last_name": null,
"apartment": "186",
"verified": true,
"join_date": "2026-03-05T03:59:57.226876",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 8725618164,
"username": "lkmthreeseven_bot",
"first_name": "DomovoyKontrolBot",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-05T03:59:57.221608",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 5475262081,
"username": null,
"first_name": "Надежда",
"last_name": "Тырина",
"apartment": "152",
"verified": true,
"join_date": "2026-02-23T16:46:55.844916",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 197957361,
"username": "ktybdsq",
"first_name": "Александр",
"last_name": "MatrixHasYou",
"apartment": "152",
"verified": true,
"join_date": "2026-02-23T13:28:08.090899",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
}
],
"messages": []
}

View file

@ -0,0 +1,185 @@
{
"export_date": "2026-03-11T18:40:38.970537",
"chat_id": null,
"period": {
"from": "2026-03-04T18:40:38.966350",
"to": "2026-03-11T18:40:38.970559"
},
"summary": {
"total_users": 12,
"verified_users": 12,
"total_messages": 3
},
"users": [
{
"user_id": 259925158,
"username": "Izuma_net",
"first_name": "izuma_net",
"last_name": null,
"apartment": "295",
"verified": true,
"join_date": "2026-03-11T10:34:58.022583",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 462572897,
"username": null,
"first_name": "Алекс",
"last_name": null,
"apartment": "48",
"verified": true,
"join_date": "2026-03-10T16:30:06.663941",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 576801733,
"username": "VolkovaElena73",
"first_name": "Елена",
"last_name": "Волкова Ульяновск",
"apartment": null,
"verified": true,
"join_date": "2026-03-10T16:06:40.293427",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1136316705,
"username": "IrinaTs15",
"first_name": "Irina",
"last_name": "Tsyganova",
"apartment": "319",
"verified": true,
"join_date": "2026-03-10T16:06:40.292007",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1970561536,
"username": null,
"first_name": "Окся",
"last_name": null,
"apartment": "255 КВ",
"verified": true,
"join_date": "2026-03-10T16:06:40.289231",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1299551170,
"username": "katt_rin9",
"first_name": "Екатерина🐈",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-05T03:59:57.232963",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1335945859,
"username": null,
"first_name": "O4aPoBaHHblu_cTpaHHuK",
"last_name": null,
"apartment": "",
"verified": true,
"join_date": "2026-03-05T03:59:57.231023",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1867403697,
"username": "cvetok_nura",
"first_name": "Светлана",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-05T03:59:57.228965",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 8181748817,
"username": null,
"first_name": "Фаина",
"last_name": null,
"apartment": "186",
"verified": true,
"join_date": "2026-03-05T03:59:57.226876",
"message_count": 1,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 8725618164,
"username": "lkmthreeseven_bot",
"first_name": "DomovoyKontrolBot",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-05T03:59:57.221608",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 5475262081,
"username": null,
"first_name": "Надежда",
"last_name": "Тырина",
"apartment": "152",
"verified": true,
"join_date": "2026-02-23T16:46:55.844916",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 197957361,
"username": "ktybdsq",
"first_name": "Александр",
"last_name": "MatrixHasYou",
"apartment": "152",
"verified": true,
"join_date": "2026-02-23T13:28:08.090899",
"message_count": 2,
"spy_score": 0,
"spy_flags": []
}
],
"messages": [
{
"message_id": 6693,
"user_id": 197957361,
"text": "162 квартира, 5й подьезд - топят всех \n\nкто нибудь знает кто живет там в 2шке ?",
"timestamp": "2026-03-11T18:27:49.681833",
"reply_to": null
},
{
"message_id": 6690,
"user_id": 197957361,
"text": "Такая куча ссылок на какие то акты - да с номерами !\nФаина явно заинтересует вам что то из актов !",
"timestamp": "2026-03-11T17:16:35.705412",
"reply_to": null
},
{
"message_id": 6689,
"user_id": 8181748817,
"text": "Это отчет УО за 2025г.\nТатьяна Мих. расклеила",
"timestamp": "2026-03-11T17:06:13.912964",
"reply_to": null
}
]
}

View file

@ -0,0 +1,206 @@
{
"export_date": "2026-03-16T18:20:48.915055",
"chat_id": null,
"period": {
"from": "2026-03-09T18:20:48.911322",
"to": "2026-03-16T18:20:48.915072"
},
"summary": {
"total_users": 12,
"verified_users": 12,
"total_messages": 6
},
"users": [
{
"user_id": 259925158,
"username": "Izuma_net",
"first_name": "izuma_net",
"last_name": null,
"apartment": "295",
"verified": true,
"join_date": "2026-03-11T10:34:58.022583",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 462572897,
"username": null,
"first_name": "Алекс",
"last_name": null,
"apartment": "48",
"verified": true,
"join_date": "2026-03-10T16:30:06.663941",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 576801733,
"username": "VolkovaElena73",
"first_name": "Елена",
"last_name": "Волкова Ульяновск",
"apartment": null,
"verified": true,
"join_date": "2026-03-10T16:06:40.293427",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1136316705,
"username": "IrinaTs15",
"first_name": "Irina",
"last_name": "Tsyganova",
"apartment": "319",
"verified": true,
"join_date": "2026-03-10T16:06:40.292007",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1970561536,
"username": null,
"first_name": "Окся",
"last_name": null,
"apartment": "255 КВ",
"verified": true,
"join_date": "2026-03-10T16:06:40.289231",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1299551170,
"username": "katt_rin9",
"first_name": "Екатерина🐈",
"last_name": null,
"apartment": "205",
"verified": true,
"join_date": "2026-03-05T03:59:57.232963",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1335945859,
"username": null,
"first_name": "O4aPoBaHHblu_cTpaHHuK",
"last_name": null,
"apartment": "",
"verified": true,
"join_date": "2026-03-05T03:59:57.231023",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1867403697,
"username": "cvetok_nura",
"first_name": "Светлана",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-05T03:59:57.228965",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 8181748817,
"username": null,
"first_name": "Фаина",
"last_name": null,
"apartment": "186",
"verified": true,
"join_date": "2026-03-05T03:59:57.226876",
"message_count": 1,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 8725618164,
"username": "lkmthreeseven_bot",
"first_name": "DomovoyKontrolBot",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-05T03:59:57.221608",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 5475262081,
"username": null,
"first_name": "Надежда",
"last_name": "Тырина",
"apartment": "152",
"verified": true,
"join_date": "2026-02-23T16:46:55.844916",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 197957361,
"username": "ktybdsq",
"first_name": "Александр",
"last_name": "MatrixHasYou",
"apartment": "152",
"verified": true,
"join_date": "2026-02-23T13:28:08.090899",
"message_count": 5,
"spy_score": 0,
"spy_flags": []
}
],
"messages": [
{
"message_id": 6699,
"user_id": 197957361,
"text": "Вот смеху то будет, если в апреле такая платежка прийдет ! 😃\n\nКак только придет первая платежка с тарифом выше 37,50 руб., подаем массовые требования о выплате штрафа 50% от суммы превышения на основании ч. 11 ст. 156 ЖК РФ\n\nНу, кто хочет платить по выдуманному тарифу может конечно при своем остаться, у нас же свобода воли полная (но не по мнению ЖСС 😁)",
"timestamp": "2026-03-12T17:04:16.699206",
"reply_to": null
},
{
"message_id": 6698,
"user_id": 197957361,
"text": "❔Кто нибудь в нашем доме понимает - каким таким образом ЖСС считает, что бумажка без подписи и печати на подьезде позволяет им навязать нам, собственникам, какой то выдуманный тариф с марта ? \n\nУже сколько они тарифов предлагали нам ? со счета сбился я😆\nВсе разные\nНи один не обоснован ничем\n\nИ вот - теперь вообще спустя рукава, бумажку из принтера повесили - вот вам новый тариф! \n\nЧто то нету таких вариантов в Жилищном Кодексе РФ, где четко написано, что этот вопрос решается только на Общем Соб",
"timestamp": "2026-03-12T17:02:51.184428",
"reply_to": null
},
{
"message_id": 6695,
"user_id": 197957361,
"text": "аварийку вызвали, перекрыли воду и уехали \nдверь никто не открывает, никого нету",
"timestamp": "2026-03-11T18:49:43.652398",
"reply_to": 6694
},
{
"message_id": 6693,
"user_id": 197957361,
"text": "162 квартира, 5й подьезд - топят всех \n\nкто нибудь знает кто живет там в 2шке ?",
"timestamp": "2026-03-11T18:27:49.681833",
"reply_to": null
},
{
"message_id": 6690,
"user_id": 197957361,
"text": "Такая куча ссылок на какие то акты - да с номерами !\nФаина явно заинтересует вам что то из актов !",
"timestamp": "2026-03-11T17:16:35.705412",
"reply_to": null
},
{
"message_id": 6689,
"user_id": 8181748817,
"text": "Это отчет УО за 2025г.\nТатьяна Мих. расклеила",
"timestamp": "2026-03-11T17:06:13.912964",
"reply_to": null
}
]
}

View file

@ -0,0 +1,725 @@
{
"export_date": "2026-03-22T23:00:00.045259",
"chat_id": null,
"period": {
"from": "2026-03-15T23:00:00.032902",
"to": "2026-03-22T23:00:00.045277"
},
"summary": {
"total_users": 27,
"verified_users": 27,
"total_messages": 51
},
"users": [
{
"user_id": 5183810334,
"username": null,
"first_name": "Леонид",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-21T21:27:30.494437",
"message_count": 1,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 5167606723,
"username": null,
"first_name": "Tatiyana",
"last_name": null,
"apartment": "297",
"verified": true,
"join_date": "2026-03-21T09:19:51.165950",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1667052403,
"username": null,
"first_name": "Ольга",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-20T11:47:26.400692",
"message_count": 1,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 5166131650,
"username": null,
"first_name": "Николай",
"last_name": "Петрович",
"apartment": null,
"verified": true,
"join_date": "2026-03-20T11:35:37.951913",
"message_count": 2,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 6344554911,
"username": null,
"first_name": "Наташа 🌹",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-19T12:26:06.709415",
"message_count": 3,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 368569899,
"username": "Countesss",
"first_name": "Анастасия",
"last_name": "Лютерова",
"apartment": null,
"verified": true,
"join_date": "2026-03-19T06:07:27.815681",
"message_count": 1,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 7339698511,
"username": null,
"first_name": "Елена",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-19T05:28:20.549649",
"message_count": 3,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 7193750921,
"username": null,
"first_name": "Светлана",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-17T14:10:20.804365",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 5246728667,
"username": "valeriya_nagini",
"first_name": "Валерия",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-17T11:44:28.288584",
"message_count": 1,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1670557872,
"username": "KhaykinaAnastasia",
"first_name": "Анастасия",
"last_name": null,
"apartment": "295",
"verified": true,
"join_date": "2026-03-17T11:44:28.284381",
"message_count": 1,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 2139751123,
"username": null,
"first_name": "Евгений",
"last_name": "Петрович",
"apartment": null,
"verified": true,
"join_date": "2026-03-17T11:44:28.265579",
"message_count": 1,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 1861008160,
"username": null,
"first_name": "Елена",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-17T11:44:28.262810",
"message_count": 1,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 1179832004,
"username": "st5ksu",
"first_name": "Kseniya",
"last_name": "Stepanova",
"apartment": null,
"verified": true,
"join_date": "2026-03-17T11:44:28.259098",
"message_count": 3,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1432214116,
"username": "pro100beze",
"first_name": "Алёна",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-17T11:44:28.256800",
"message_count": 1,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 861072224,
"username": "AFadeeva",
"first_name": "Александра🌤",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-17T11:44:28.228806",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 259925158,
"username": "Izuma_net",
"first_name": "izuma_net",
"last_name": null,
"apartment": "295",
"verified": true,
"join_date": "2026-03-11T10:34:58.022583",
"message_count": 1,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 462572897,
"username": null,
"first_name": "Алекс",
"last_name": null,
"apartment": "48",
"verified": true,
"join_date": "2026-03-10T16:30:06.663941",
"message_count": 1,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 576801733,
"username": "VolkovaElena73",
"first_name": "Елена",
"last_name": "Волкова Ульяновск",
"apartment": null,
"verified": true,
"join_date": "2026-03-10T16:06:40.293427",
"message_count": 1,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1136316705,
"username": "IrinaTs15",
"first_name": "Irina",
"last_name": "Tsyganova",
"apartment": "319",
"verified": true,
"join_date": "2026-03-10T16:06:40.292007",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1970561536,
"username": null,
"first_name": "Окся",
"last_name": null,
"apartment": "255 КВ",
"verified": true,
"join_date": "2026-03-10T16:06:40.289231",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1299551170,
"username": "katt_rin9",
"first_name": "Екатерина🐈",
"last_name": null,
"apartment": "205",
"verified": true,
"join_date": "2026-03-05T03:59:57.232963",
"message_count": 1,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1335945859,
"username": null,
"first_name": "O4aPoBaHHblu_cTpaHHuK",
"last_name": null,
"apartment": "",
"verified": true,
"join_date": "2026-03-05T03:59:57.231023",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1867403697,
"username": "cvetok_nura",
"first_name": "Светлана",
"last_name": null,
"apartment": "171",
"verified": true,
"join_date": "2026-03-05T03:59:57.228965",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 8181748817,
"username": null,
"first_name": "Фаина",
"last_name": null,
"apartment": "186",
"verified": true,
"join_date": "2026-03-05T03:59:57.226876",
"message_count": 10,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 8725618164,
"username": "lkmthreeseven_bot",
"first_name": "DomovoyKontrolBot",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-05T03:59:57.221608",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 5475262081,
"username": null,
"first_name": "Надежда",
"last_name": "Тырина",
"apartment": "152",
"verified": true,
"join_date": "2026-02-23T16:46:55.844916",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 197957361,
"username": "ktybdsq",
"first_name": "Александр",
"last_name": "MatrixHasYou",
"apartment": "152",
"verified": true,
"join_date": "2026-02-23T13:28:08.090899",
"message_count": 16,
"spy_score": 0,
"spy_flags": []
}
],
"messages": [
{
"message_id": 6811,
"user_id": 197957361,
"text": null,
"timestamp": "2026-03-22T11:38:52.034326",
"reply_to": 6260
},
{
"message_id": 6810,
"user_id": 197957361,
"text": null,
"timestamp": "2026-03-22T11:38:52.025450",
"reply_to": 6260
},
{
"message_id": 6809,
"user_id": 1179832004,
"text": "А вы наверное юморист?",
"timestamp": "2026-03-22T04:46:48.764565",
"reply_to": 6808
},
{
"message_id": 6808,
"user_id": 5183810334,
"text": "Вы что уличный сторож?",
"timestamp": "2026-03-21T21:27:31.050046",
"reply_to": null
},
{
"message_id": 6807,
"user_id": 1179832004,
"text": null,
"timestamp": "2026-03-21T19:25:04.858395",
"reply_to": 6266
},
{
"message_id": 6806,
"user_id": 197957361,
"text": "А за уборкой подьезда и заменой лампочек в подьезде - видимо тоже куда то надо обращаться ? \n\nУ меня ощущение у одного, что ЖСС как бы и не при чем ? \n\nНу старается показать что не при чем 😅",
"timestamp": "2026-03-21T12:10:14.825495",
"reply_to": null
},
{
"message_id": 6805,
"user_id": 8181748817,
"text": "Уважаемые жители дома!\nКто в янв- февр- марте обращался за доступом к видеоархиву и получил его? А кто не получил?",
"timestamp": "2026-03-21T12:07:50.673653",
"reply_to": null
},
{
"message_id": 6804,
"user_id": 8181748817,
"text": "Я пишу: \"если с января...\" так как в чате жители писали, что не могут получить доступ к видео.\nИ в январе исполнитель с Видеосервис говорил, что с января 26г.доступ к архиву с разрешения Погодина.",
"timestamp": "2026-03-21T12:04:13.443501",
"reply_to": 6803
},
{
"message_id": 6803,
"user_id": 462572897,
"text": "Фаина, а как понять, что с января 26 г. услуга не оказывается?",
"timestamp": "2026-03-21T11:50:50.112523",
"reply_to": 6799
},
{
"message_id": 6802,
"user_id": 8181748817,
"text": "Там приложено Доп.согл.11 от 01.02.24г, которым наш дом присоединился",
"timestamp": "2026-03-21T11:00:57.340571",
"reply_to": 6798
},
{
"message_id": 6801,
"user_id": 197957361,
"text": "да ПСД дома подпишет все, что ей ЖСС даст, не глядя и забыв про бухгалтерию 😀😂",
"timestamp": "2026-03-21T10:59:18.563614",
"reply_to": 6799
},
{
"message_id": 6800,
"user_id": 197957361,
"text": "Какой же нормальный человек будет в магазине покупая диван, заключать договор не между собой и магазином - а подписывать какой то договор между Магазином и другим человеком ? \nОн потом как в суд пойдет если что случится ? \nВ суде пальцем у виска покрутят и скажут \"Это ж не ваш договор, чего вы на него ссылаетесь ? \"",
"timestamp": "2026-03-21T10:58:50.056137",
"reply_to": null
},
{
"message_id": 6799,
"user_id": 8181748817,
"text": "Если с января 26г услуга не оказывается, почему Акты ПСДома подписывает. \nЯ лично за янв- февр.за камеры не плачу.",
"timestamp": "2026-03-21T10:58:35.386487",
"reply_to": null
},
{
"message_id": 6798,
"user_id": 197957361,
"text": null,
"timestamp": "2026-03-21T10:57:54.609831",
"reply_to": null
},
{
"message_id": 6797,
"user_id": 8181748817,
"text": "Жители дома при инцидентах обращались к Т.Мих, так как договора никто ранее и не видел, кроме ПСдома.",
"timestamp": "2026-03-21T10:54:32.497882",
"reply_to": null
},
{
"message_id": 6796,
"user_id": 8181748817,
"text": "Вот договор, который нам дали на руки в УО ЖСС 27 февраля. В ГИС ЖКХ его нет.\nПо п.4.1.3. сказано, что запись из архива предоставляется по письменным запросам собственников МКД непосредственно в ООО \"ВидеоСервис\".\nПо п.2.2. записано, что Акты выполнкнеых работ подписывает Исполнитель, Заквзчик и ПСДома, на основании которых ежемесячно производится оплата.",
"timestamp": "2026-03-21T10:50:22.689484",
"reply_to": null
},
{
"message_id": 6794,
"user_id": 8181748817,
"text": null,
"timestamp": "2026-03-21T10:40:53.162184",
"reply_to": null
},
{
"message_id": 6795,
"user_id": 8181748817,
"text": null,
"timestamp": "2026-03-21T10:40:53.117680",
"reply_to": null
},
{
"message_id": 6793,
"user_id": 8181748817,
"text": null,
"timestamp": "2026-03-21T10:40:53.080101",
"reply_to": null
},
{
"message_id": 6792,
"user_id": 197957361,
"text": null,
"timestamp": "2026-03-21T09:36:37.219173",
"reply_to": 6262
},
{
"message_id": 6791,
"user_id": 197957361,
"text": null,
"timestamp": "2026-03-21T07:47:26.209479",
"reply_to": 6260
},
{
"message_id": 6790,
"user_id": 197957361,
"text": null,
"timestamp": "2026-03-21T07:45:12.249018",
"reply_to": null
},
{
"message_id": 6787,
"user_id": 8181748817,
"text": null,
"timestamp": "2026-03-20T14:14:46.161576",
"reply_to": 6260
},
{
"message_id": 6789,
"user_id": 8181748817,
"text": null,
"timestamp": "2026-03-20T14:14:46.136436",
"reply_to": 6260
},
{
"message_id": 6786,
"user_id": 8181748817,
"text": null,
"timestamp": "2026-03-20T14:14:46.113065",
"reply_to": 6260
},
{
"message_id": 6788,
"user_id": 8181748817,
"text": null,
"timestamp": "2026-03-20T14:14:46.089582",
"reply_to": 6260
},
{
"message_id": 6785,
"user_id": 8181748817,
"text": null,
"timestamp": "2026-03-20T14:14:46.056946",
"reply_to": 6260
},
{
"message_id": 6784,
"user_id": 1667052403,
"text": "Вы о чём?",
"timestamp": "2026-03-20T11:47:26.526343",
"reply_to": 6266
},
{
"message_id": 6783,
"user_id": 5166131650,
"text": "Напротив 6 подъезда?",
"timestamp": "2026-03-20T11:37:13.711268",
"reply_to": 6266
},
{
"message_id": 6782,
"user_id": 5166131650,
"text": "Напротив 5 подъезда?",
"timestamp": "2026-03-20T11:35:38.550863",
"reply_to": 6266
},
{
"message_id": 6781,
"user_id": 6344554911,
"text": "Его Алексей зовут",
"timestamp": "2026-03-19T15:32:23.718845",
"reply_to": 6780
},
{
"message_id": 6780,
"user_id": 197957361,
"text": "А как его зовут ? Может его в бота в раздел Мастера записать ?",
"timestamp": "2026-03-19T15:28:58.258357",
"reply_to": 6778
},
{
"message_id": 6779,
"user_id": 6344554911,
"text": "Спасибо",
"timestamp": "2026-03-19T12:48:10.923728",
"reply_to": null
},
{
"message_id": 6778,
"user_id": 259925158,
"text": "Попробуйте этот 8 (996) 219-95-67",
"timestamp": "2026-03-19T12:45:37.451726",
"reply_to": 6777
},
{
"message_id": 6777,
"user_id": 6344554911,
"text": "Здравствуйте. Кто знает телефон нашего сантехника, дайте пожалуйста",
"timestamp": "2026-03-19T12:26:07.174734",
"reply_to": null
},
{
"message_id": 6776,
"user_id": 7339698511,
"text": "Спасибо большое",
"timestamp": "2026-03-19T06:17:47.232121",
"reply_to": 6775
},
{
"message_id": 6775,
"user_id": 368569899,
"text": "Елена, я вам написала в личные сообщения",
"timestamp": "2026-03-19T06:07:28.399172",
"reply_to": 6773
},
{
"message_id": 6774,
"user_id": 7339698511,
"text": "Или напишите адреса куда можно обратиться по поводу камер и домофона, пожалуйста",
"timestamp": "2026-03-19T05:29:12.037940",
"reply_to": null
},
{
"message_id": 6773,
"user_id": 7339698511,
"text": "Люди!!! Дайте ссылки на домофон и на камеры???",
"timestamp": "2026-03-19T05:28:21.047935",
"reply_to": null
},
{
"message_id": 6772,
"user_id": 197957361,
"text": null,
"timestamp": "2026-03-17T17:59:33.059721",
"reply_to": 6262
},
{
"message_id": 6770,
"user_id": 1299551170,
"text": "Через госуслуги попробуйте связаться",
"timestamp": "2026-03-17T11:44:29.175932",
"reply_to": 6769
},
{
"message_id": 6767,
"user_id": 2139751123,
"text": "Отписка пять баллов 👍",
"timestamp": "2026-03-17T11:44:28.974797",
"reply_to": 6766
},
{
"message_id": 6771,
"user_id": 5246728667,
"text": "Забрали уже.",
"timestamp": "2026-03-17T11:44:28.952646",
"reply_to": null
},
{
"message_id": 6766,
"user_id": 1861008160,
"text": null,
"timestamp": "2026-03-17T11:44:28.923567",
"reply_to": null
},
{
"message_id": 6761,
"user_id": 1670557872,
"text": "Понятно, спасибо",
"timestamp": "2026-03-17T11:44:28.903963",
"reply_to": null
},
{
"message_id": 6762,
"user_id": 576801733,
"text": "Он выдаёт мне на андроид опять vdome, а оно не пашет((((",
"timestamp": "2026-03-17T11:44:28.863145",
"reply_to": 6756
},
{
"message_id": 6769,
"user_id": 1432214116,
"text": null,
"timestamp": "2026-03-17T11:44:28.763231",
"reply_to": 6266
},
{
"message_id": 6763,
"user_id": 1179832004,
"text": "Значит вам нужно на Киевский,они установят вам приложение",
"timestamp": "2026-03-17T11:44:28.742319",
"reply_to": 6762
},
{
"message_id": 6764,
"user_id": 576801733,
"text": "Спасибки, завтра доеду)))",
"timestamp": "2026-03-17T11:44:28.723239",
"reply_to": 6763
},
{
"message_id": 6768,
"user_id": 8181748817,
"text": "Смешной штраф🙈",
"timestamp": "2026-03-17T11:44:28.638147",
"reply_to": null
},
{
"message_id": 6765,
"user_id": 197957361,
"text": null,
"timestamp": "2026-03-17T11:44:28.416187",
"reply_to": 6764
}
]
}

View file

@ -0,0 +1,668 @@
{
"export_date": "2026-03-29T23:00:00.120135",
"chat_id": null,
"period": {
"from": "2026-03-22T23:00:00.118218",
"to": "2026-03-29T23:00:00.120151"
},
"summary": {
"total_users": 32,
"verified_users": 27,
"total_messages": 33
},
"users": [
{
"user_id": 1476737008,
"username": null,
"first_name": "Елена",
"last_name": null,
"apartment": null,
"verified": false,
"join_date": "2026-03-28T17:29:19.435202",
"message_count": 2,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 5036016769,
"username": null,
"first_name": "Алсу",
"last_name": "Юсупова",
"apartment": null,
"verified": false,
"join_date": "2026-03-27T16:54:14.815446",
"message_count": 2,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 864147545,
"username": "roman_garipov",
"first_name": "Roman",
"last_name": null,
"apartment": null,
"verified": false,
"join_date": "2026-03-25T16:59:41.548812",
"message_count": 2,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 6654418281,
"username": "snez_frolova99",
"first_name": "Твоя ногтевая фея💅🏼",
"last_name": null,
"apartment": null,
"verified": false,
"join_date": "2026-03-24T15:02:19.685046",
"message_count": 1,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 2044370304,
"username": "Brasletvz73",
"first_name": "Браслет",
"last_name": null,
"apartment": null,
"verified": false,
"join_date": "2026-03-24T14:50:37.799662",
"message_count": 4,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 5183810334,
"username": null,
"first_name": "Леонид",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-21T21:27:30.494437",
"message_count": 1,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 5167606723,
"username": null,
"first_name": "Tatiyana",
"last_name": null,
"apartment": "297",
"verified": true,
"join_date": "2026-03-21T09:19:51.165950",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1667052403,
"username": null,
"first_name": "Ольга",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-20T11:47:26.400692",
"message_count": 6,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 5166131650,
"username": null,
"first_name": "Николай",
"last_name": "Петрович",
"apartment": null,
"verified": true,
"join_date": "2026-03-20T11:35:37.951913",
"message_count": 2,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 6344554911,
"username": null,
"first_name": "Наташа 🌹",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-19T12:26:06.709415",
"message_count": 3,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 368569899,
"username": "Countesss",
"first_name": "Анастасия",
"last_name": "Лютерова",
"apartment": null,
"verified": true,
"join_date": "2026-03-19T06:07:27.815681",
"message_count": 2,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 7339698511,
"username": null,
"first_name": "Елена",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-19T05:28:20.549649",
"message_count": 3,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 7193750921,
"username": null,
"first_name": "Светлана",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-17T14:10:20.804365",
"message_count": 2,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 5246728667,
"username": "valeriya_nagini",
"first_name": "Валерия",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-17T11:44:28.288584",
"message_count": 1,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1670557872,
"username": "KhaykinaAnastasia",
"first_name": "Анастасия",
"last_name": null,
"apartment": "295",
"verified": true,
"join_date": "2026-03-17T11:44:28.284381",
"message_count": 5,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 2139751123,
"username": null,
"first_name": "Евгений",
"last_name": "Петрович",
"apartment": null,
"verified": true,
"join_date": "2026-03-17T11:44:28.265579",
"message_count": 1,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 1861008160,
"username": null,
"first_name": "Елена",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-17T11:44:28.262810",
"message_count": 1,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 1179832004,
"username": "st5ksu",
"first_name": "Kseniya",
"last_name": "Stepanova",
"apartment": null,
"verified": true,
"join_date": "2026-03-17T11:44:28.259098",
"message_count": 4,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1432214116,
"username": "pro100beze",
"first_name": "Алёна",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-17T11:44:28.256800",
"message_count": 1,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 861072224,
"username": "AFadeeva",
"first_name": "Александра🌤",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-17T11:44:28.228806",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 259925158,
"username": "Izuma_net",
"first_name": "izuma_net",
"last_name": null,
"apartment": "295",
"verified": true,
"join_date": "2026-03-11T10:34:58.022583",
"message_count": 1,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 462572897,
"username": null,
"first_name": "Алекс",
"last_name": null,
"apartment": "48",
"verified": true,
"join_date": "2026-03-10T16:30:06.663941",
"message_count": 1,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 576801733,
"username": "VolkovaElena73",
"first_name": "Елена",
"last_name": "Волкова Ульяновск",
"apartment": null,
"verified": true,
"join_date": "2026-03-10T16:06:40.293427",
"message_count": 1,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1136316705,
"username": "IrinaTs15",
"first_name": "Irina",
"last_name": "Tsyganova",
"apartment": "319",
"verified": true,
"join_date": "2026-03-10T16:06:40.292007",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1970561536,
"username": null,
"first_name": "Окся",
"last_name": null,
"apartment": "255 КВ",
"verified": true,
"join_date": "2026-03-10T16:06:40.289231",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1299551170,
"username": "katt_rin9",
"first_name": "Екатерина🐈",
"last_name": null,
"apartment": "205",
"verified": true,
"join_date": "2026-03-05T03:59:57.232963",
"message_count": 1,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1335945859,
"username": null,
"first_name": "O4aPoBaHHblu_cTpaHHuK",
"last_name": null,
"apartment": "",
"verified": true,
"join_date": "2026-03-05T03:59:57.231023",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1867403697,
"username": "cvetok_nura",
"first_name": "Светлана",
"last_name": null,
"apartment": "171",
"verified": true,
"join_date": "2026-03-05T03:59:57.228965",
"message_count": 4,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 8181748817,
"username": null,
"first_name": "Фаина",
"last_name": null,
"apartment": "186",
"verified": true,
"join_date": "2026-03-05T03:59:57.226876",
"message_count": 10,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 8725618164,
"username": "lkmthreeseven_bot",
"first_name": "DomovoyKontrolBot",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-05T03:59:57.221608",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 5475262081,
"username": null,
"first_name": "Надежда",
"last_name": "Тырина",
"apartment": "152",
"verified": true,
"join_date": "2026-02-23T16:46:55.844916",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 197957361,
"username": "ktybdsq",
"first_name": "Александр",
"last_name": "MatrixHasYou",
"apartment": "152",
"verified": true,
"join_date": "2026-02-23T13:28:08.090899",
"message_count": 21,
"spy_score": 0,
"spy_flags": []
}
],
"messages": [
{
"message_id": 6845,
"user_id": 1667052403,
"text": "https://ulpressa.ru/2026/03/28/upravdom-provajdery-v-mkd-2-dlya-kogo-zakony-pisany/",
"timestamp": "2026-03-29T18:21:13.686290",
"reply_to": null
},
{
"message_id": 6844,
"user_id": 1667052403,
"text": null,
"timestamp": "2026-03-29T18:03:20.251284",
"reply_to": null
},
{
"message_id": 6843,
"user_id": 1476737008,
"text": "Спасибо большое)",
"timestamp": "2026-03-29T06:46:04.110315",
"reply_to": 6840
},
{
"message_id": 6842,
"user_id": 1670557872,
"text": ")))",
"timestamp": "2026-03-29T06:12:19.450560",
"reply_to": 6262
},
{
"message_id": 6841,
"user_id": 197957361,
"text": null,
"timestamp": "2026-03-29T06:10:46.673382",
"reply_to": 6262
},
{
"message_id": 6840,
"user_id": 368569899,
"text": "Добрый вечер. Мы пару раз делали в Лидере на 1 этаже, там где ключи делают.",
"timestamp": "2026-03-28T17:37:29.003831",
"reply_to": 6839
},
{
"message_id": 6839,
"user_id": 1476737008,
"text": "Добрый вечер.Соседи,подскажите пожалуйста где сделать ключ от домофона",
"timestamp": "2026-03-28T17:29:19.880276",
"reply_to": null
},
{
"message_id": 6838,
"user_id": 5036016769,
"text": "Где же вы все были когда снег там лежал",
"timestamp": "2026-03-27T16:54:45.860697",
"reply_to": 6266
},
{
"message_id": 6836,
"user_id": 5036016769,
"text": null,
"timestamp": "2026-03-27T16:54:15.376935",
"reply_to": 6266
},
{
"message_id": 6835,
"user_id": 197957361,
"text": null,
"timestamp": "2026-03-27T15:01:39.815229",
"reply_to": 6262
},
{
"message_id": 6834,
"user_id": 1667052403,
"text": "Да да\nИнета нет\nА платим исправно....",
"timestamp": "2026-03-26T08:32:56.795527",
"reply_to": 6262
},
{
"message_id": 6833,
"user_id": 197957361,
"text": null,
"timestamp": "2026-03-26T08:25:52.691712",
"reply_to": 6262
},
{
"message_id": 6832,
"user_id": 864147545,
"text": null,
"timestamp": "2026-03-25T17:59:17.637774",
"reply_to": null
},
{
"message_id": 6831,
"user_id": 864147545,
"text": "У718АС 73 хендай элантра не нужен о заезжать на «газон»(в нашем случае грязь)",
"timestamp": "2026-03-25T16:59:42.057708",
"reply_to": null
},
{
"message_id": 6830,
"user_id": 1667052403,
"text": "Добрый вечер\nУ всех сегодня билайн домашний инет не работал?\n\nА теперь всё с ограничениями?",
"timestamp": "2026-03-25T15:47:14.973788",
"reply_to": null
},
{
"message_id": 6829,
"user_id": 1179832004,
"text": null,
"timestamp": "2026-03-25T14:24:10.506225",
"reply_to": 6266
},
{
"message_id": 6828,
"user_id": 1670557872,
"text": "Ой как здорово",
"timestamp": "2026-03-25T09:37:18.521901",
"reply_to": 6825
},
{
"message_id": 6827,
"user_id": 7193750921,
"text": "Цены старые, конечно.",
"timestamp": "2026-03-25T09:25:22.994905",
"reply_to": null
},
{
"message_id": 6826,
"user_id": 1867403697,
"text": "300 р",
"timestamp": "2026-03-25T09:25:22.294027",
"reply_to": null
},
{
"message_id": 6825,
"user_id": 1867403697,
"text": "Делаешь им фотку счетчика они присылают акт проверки моментально",
"timestamp": "2026-03-25T09:25:12.232124",
"reply_to": null
},
{
"message_id": 6824,
"user_id": 7193750921,
"text": null,
"timestamp": "2026-03-25T09:24:33.141258",
"reply_to": null
},
{
"message_id": 6823,
"user_id": 1670557872,
"text": "Большое спасибо!",
"timestamp": "2026-03-25T09:24:23.524322",
"reply_to": null
},
{
"message_id": 6822,
"user_id": 1867403697,
"text": "Мне вот они понравились )",
"timestamp": "2026-03-25T09:24:06.325785",
"reply_to": null
},
{
"message_id": 6821,
"user_id": 1867403697,
"text": "+7 927 270-21-70 добрый день",
"timestamp": "2026-03-25T09:23:59.518450",
"reply_to": null
},
{
"message_id": 6820,
"user_id": 1670557872,
"text": "Добрый день! Подскажите пожалуйста, кто-нибудь знает номер телефона организации по поверке счетчиков",
"timestamp": "2026-03-25T09:22:49.504050",
"reply_to": null
},
{
"message_id": 6819,
"user_id": 197957361,
"text": null,
"timestamp": "2026-03-25T08:25:50.145257",
"reply_to": 6262
},
{
"message_id": 6818,
"user_id": 2044370304,
"text": "Как в их чат написать ?",
"timestamp": "2026-03-24T15:15:45.859199",
"reply_to": null
},
{
"message_id": 6817,
"user_id": 1667052403,
"text": "Пишите в чат 5 дома. Может там хозяин",
"timestamp": "2026-03-24T15:05:27.186789",
"reply_to": null
},
{
"message_id": 6816,
"user_id": 6654418281,
"text": "Эвакуатор может вызвать ?",
"timestamp": "2026-03-24T15:02:20.242206",
"reply_to": null
},
{
"message_id": 6815,
"user_id": 2044370304,
"text": null,
"timestamp": "2026-03-24T14:53:40.014838",
"reply_to": null
},
{
"message_id": 6814,
"user_id": 2044370304,
"text": "Товарищи, кто знает хозяина этой машины ?",
"timestamp": "2026-03-24T14:53:36.062481",
"reply_to": null
},
{
"message_id": 6813,
"user_id": 2044370304,
"text": null,
"timestamp": "2026-03-24T14:50:38.299476",
"reply_to": 6266
},
{
"message_id": 6812,
"user_id": 197957361,
"text": null,
"timestamp": "2026-03-23T10:04:38.017104",
"reply_to": 6262
}
]
}

View file

@ -0,0 +1,623 @@
{
"export_date": "2026-04-05T23:00:00.045504",
"chat_id": null,
"period": {
"from": "2026-03-29T23:00:00.035069",
"to": "2026-04-05T23:00:00.045519"
},
"summary": {
"total_users": 33,
"verified_users": 33,
"total_messages": 24
},
"users": [
{
"user_id": 772234481,
"username": null,
"first_name": "Анастасия",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-04-05T14:01:56.745028",
"message_count": 3,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 1476737008,
"username": null,
"first_name": "Елена",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-28T17:29:19.435202",
"message_count": 2,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 5036016769,
"username": null,
"first_name": "Алсу",
"last_name": "Юсупова",
"apartment": null,
"verified": true,
"join_date": "2026-03-27T16:54:14.815446",
"message_count": 2,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 864147545,
"username": "roman_garipov",
"first_name": "Roman",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-25T16:59:41.548812",
"message_count": 2,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 6654418281,
"username": "snez_frolova99",
"first_name": "Твоя ногтевая фея💅🏼",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-24T15:02:19.685046",
"message_count": 1,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 2044370304,
"username": "Brasletvz73",
"first_name": "Браслет",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-24T14:50:37.799662",
"message_count": 4,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 5183810334,
"username": null,
"first_name": "Леонид",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-21T21:27:30.494437",
"message_count": 1,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 5167606723,
"username": null,
"first_name": "Tatiyana",
"last_name": null,
"apartment": "297",
"verified": true,
"join_date": "2026-03-21T09:19:51.165950",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1667052403,
"username": null,
"first_name": "Ольга",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-20T11:47:26.400692",
"message_count": 10,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 5166131650,
"username": null,
"first_name": "Николай",
"last_name": "Петрович",
"apartment": null,
"verified": true,
"join_date": "2026-03-20T11:35:37.951913",
"message_count": 2,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 6344554911,
"username": null,
"first_name": "Наташа 🌹",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-19T12:26:06.709415",
"message_count": 3,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 368569899,
"username": "Countesss",
"first_name": "Анастасия",
"last_name": "Лютерова",
"apartment": null,
"verified": true,
"join_date": "2026-03-19T06:07:27.815681",
"message_count": 2,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 7339698511,
"username": null,
"first_name": "Елена",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-19T05:28:20.549649",
"message_count": 3,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 7193750921,
"username": null,
"first_name": "Светлана",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-17T14:10:20.804365",
"message_count": 2,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 5246728667,
"username": "valeriya_nagini",
"first_name": "Валерия",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-17T11:44:28.288584",
"message_count": 1,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1670557872,
"username": "KhaykinaAnastasia",
"first_name": "Анастасия",
"last_name": null,
"apartment": "295",
"verified": true,
"join_date": "2026-03-17T11:44:28.284381",
"message_count": 5,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 2139751123,
"username": null,
"first_name": "Евгений",
"last_name": "Петрович",
"apartment": null,
"verified": true,
"join_date": "2026-03-17T11:44:28.265579",
"message_count": 1,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 1861008160,
"username": null,
"first_name": "Елена",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-17T11:44:28.262810",
"message_count": 1,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 1179832004,
"username": "st5ksu",
"first_name": "Kseniya",
"last_name": "Stepanova",
"apartment": null,
"verified": true,
"join_date": "2026-03-17T11:44:28.259098",
"message_count": 4,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1432214116,
"username": "pro100beze",
"first_name": "Алёна",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-17T11:44:28.256800",
"message_count": 1,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 861072224,
"username": "AFadeeva",
"first_name": "Александра🌤",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-17T11:44:28.228806",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 259925158,
"username": "Izuma_net",
"first_name": "izuma_net",
"last_name": null,
"apartment": "295",
"verified": true,
"join_date": "2026-03-11T10:34:58.022583",
"message_count": 1,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 462572897,
"username": null,
"first_name": "Алекс",
"last_name": null,
"apartment": "48",
"verified": true,
"join_date": "2026-03-10T16:30:06.663941",
"message_count": 1,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 576801733,
"username": "VolkovaElena73",
"first_name": "Елена",
"last_name": "Волкова Ульяновск",
"apartment": null,
"verified": true,
"join_date": "2026-03-10T16:06:40.293427",
"message_count": 4,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1136316705,
"username": "IrinaTs15",
"first_name": "Irina",
"last_name": "Tsyganova",
"apartment": "319",
"verified": true,
"join_date": "2026-03-10T16:06:40.292007",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1970561536,
"username": null,
"first_name": "Окся",
"last_name": null,
"apartment": "255 КВ",
"verified": true,
"join_date": "2026-03-10T16:06:40.289231",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1299551170,
"username": "katt_rin9",
"first_name": "Екатерина🐈",
"last_name": null,
"apartment": "205",
"verified": true,
"join_date": "2026-03-05T03:59:57.232963",
"message_count": 1,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1335945859,
"username": null,
"first_name": "O4aPoBaHHblu_cTpaHHuK",
"last_name": null,
"apartment": "",
"verified": true,
"join_date": "2026-03-05T03:59:57.231023",
"message_count": 2,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 1867403697,
"username": "cvetok_nura",
"first_name": "Светлана",
"last_name": null,
"apartment": "171",
"verified": true,
"join_date": "2026-03-05T03:59:57.228965",
"message_count": 4,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 8181748817,
"username": null,
"first_name": "Фаина",
"last_name": null,
"apartment": "186",
"verified": true,
"join_date": "2026-03-05T03:59:57.226876",
"message_count": 10,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 8725618164,
"username": "lkmthreeseven_bot",
"first_name": "DomovoyKontrolBot",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-05T03:59:57.221608",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 5475262081,
"username": null,
"first_name": "Надежда",
"last_name": "Тырина",
"apartment": "152",
"verified": true,
"join_date": "2026-02-23T16:46:55.844916",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 197957361,
"username": "ktybdsq",
"first_name": "Александр",
"last_name": "MatrixHasYou",
"apartment": "152",
"verified": true,
"join_date": "2026-02-23T13:28:08.090899",
"message_count": 30,
"spy_score": 0,
"spy_flags": []
}
],
"messages": [
{
"message_id": 6886,
"user_id": 1667052403,
"text": "🔥🔥🔥",
"timestamp": "2026-04-05T16:32:19.532647",
"reply_to": 6885
},
{
"message_id": 6885,
"user_id": 197957361,
"text": "🔥Для жильцов нашего дома поясню\nНа прошлой неделе, благодаря инициативной группе, я побывал в отделении ОБЭП\nКоторое начало расследовать \"А что тут происходит в доме вообще?\"\nВсе эти таинственные пропадающие договора, все эти документы которых нигде нет, вся эта кутерьма с видеокамерами, вся эта ерунда с оплатой домофона самовольной по метражу, по обрезкам, по ремонтам, по уборке тракторами и метлами. \n\nУ нас за года СТОЛЬКО претензий к УО ЖСС накопилось!\n\nИ вот как то на встречу они совершенно ",
"timestamp": "2026-04-05T16:13:58.099823",
"reply_to": null
},
{
"message_id": 6884,
"user_id": 197957361,
"text": "а вот и первое сообщение о платежке в почтовом ящике - и там тариф, который не мы с вами НЕ ВЫБИРАЛИ",
"timestamp": "2026-04-05T16:13:55.637380",
"reply_to": null
},
{
"message_id": 6883,
"user_id": 1667052403,
"text": "💯",
"timestamp": "2026-04-05T15:40:06.767521",
"reply_to": 6879
},
{
"message_id": 6882,
"user_id": 197957361,
"text": "Или тоже, как с камерами. Все не при чем, молиться в сторону ульяновского 2, и только по троекратному письменному разрешению неизвестно кого ? 🤣",
"timestamp": "2026-04-05T15:33:54.688085",
"reply_to": null
},
{
"message_id": 6881,
"user_id": 576801733,
"text": "Уважаемые соседи, можно вопрос не про мусор, видео кто нибудь оплачивает? У меня в этом году какой то просто адский тариф и за 2 месяца 900 с лишним рублей((( подскажите плиз тариф у нас какой? А Домофон у кого нибудь вообще работает не с ключа?",
"timestamp": "2026-04-05T15:33:40.638091",
"reply_to": null
},
{
"message_id": 6880,
"user_id": 197957361,
"text": "А у нас разве вывоз такого мусора - никак не регламентирован ? \nВот просто не верю, по простой причине - оплата то у нас за это идет не абы как, а регулярная и постоянная.\nТак что и график там должен быть регулярный и постоянный",
"timestamp": "2026-04-05T15:33:08.750641",
"reply_to": null
},
{
"message_id": 6879,
"user_id": 1335945859,
"text": "Потому что тот кто определяет эту необходимость делает это так как считает нужным именно он (она)",
"timestamp": "2026-04-05T15:31:32.428514",
"reply_to": 6878
},
{
"message_id": 6878,
"user_id": 1667052403,
"text": "Это же и про подъезды можно сказать... Увозили бы раз в неделю и разговоров бы не было\n\nКак и с уборкой подъездов и двора.\nФормулировка - по необходимости - приводит к грязи..и хаосу...",
"timestamp": "2026-04-05T15:26:34.820124",
"reply_to": null
},
{
"message_id": 6877,
"user_id": 1335945859,
"text": "Если бы увозили с такой периодичностью, чтобы не образовывалась СВАЛКА у подъезда, никто бы, я думаю, не возмущался",
"timestamp": "2026-04-05T15:24:23.779266",
"reply_to": 6875
},
{
"message_id": 6876,
"user_id": 197957361,
"text": null,
"timestamp": "2026-04-05T15:05:16.857463",
"reply_to": 6262
},
{
"message_id": 6875,
"user_id": 772234481,
"text": "У 7,8,9 подъезда есть места(выход к бакам)куда можно выкидывать крупногабаритный мусор,туда уборочные машины почему-то приезжают чаще и забирают,но опять таки,когда жильцы относили мусор туда,другие жильцы из этих подъездов ругались, и высказывали своё негодование,мол зачем весь мусор относят к их подъезду",
"timestamp": "2026-04-05T14:53:26.450162",
"reply_to": null
},
{
"message_id": 6874,
"user_id": 772234481,
"text": "Про бытовой мусор с кухни согласна,это дикость",
"timestamp": "2026-04-05T14:46:27.710436",
"reply_to": null
},
{
"message_id": 6873,
"user_id": 197957361,
"text": "у нас внутри двора рекреации под это имеются\nи это отродясь не вызывало проблем никому и никогда \n\nно вот с появлением новых жильцов такое бывает, например у 5го подьезда это тупо бытовой мусор с кухни! \nнадо быть очень диким и далеким от цивилизации, чтобы выбрасывать такой мусор - под дверь подьезда",
"timestamp": "2026-04-05T14:34:07.586221",
"reply_to": 6872
},
{
"message_id": 6872,
"user_id": 772234481,
"text": "Не всем легко эти большие баулы с мусором относить за дом,потому что они тяжелые.Я не понимаю постоянные претензии по поводу этого.(К мешкам я не причастна,но эта тема из раза в раз оговаривается в чате,зачем-не знаю,мне кажется и так все всё видят и понимают,не могу сказать точно когда,но раз в неделю приезжает машина и забирает такой мусор,можно к этому и по спокойнее относиться)\nПусть тогда все салоны/магазины/пвз и тд со стороны проспекта,куда нужно относить мусор по вашему, закроют и уберут",
"timestamp": "2026-04-05T14:01:57.183265",
"reply_to": null
},
{
"message_id": 6871,
"user_id": 576801733,
"text": "Крыши... т9 зараза",
"timestamp": "2026-04-05T13:44:46.797929",
"reply_to": null
},
{
"message_id": 6870,
"user_id": 576801733,
"text": "Искренне верим что завтра вывезут... я так и не поняла зачем они сложили мешки и пропали.... с кры я имею ввиду",
"timestamp": "2026-04-05T13:44:27.580631",
"reply_to": null
},
{
"message_id": 6869,
"user_id": 197957361,
"text": "А как дела обстоят в ваших подъездах? \nЕсть такие \"дремучие\", которые никак не поймут, что с мусором делать? \n\nМожет у кого то из окна пакеты выбрасывают ? 😁",
"timestamp": "2026-04-05T13:38:59.369432",
"reply_to": null
},
{
"message_id": 6866,
"user_id": 197957361,
"text": null,
"timestamp": "2026-04-05T13:38:06.107622",
"reply_to": null
},
{
"message_id": 6867,
"user_id": 197957361,
"text": null,
"timestamp": "2026-04-05T13:38:06.092502",
"reply_to": null
},
{
"message_id": 6868,
"user_id": 197957361,
"text": null,
"timestamp": "2026-04-05T13:38:06.074054",
"reply_to": null
},
{
"message_id": 6855,
"user_id": 1667052403,
"text": "К старшей и в ук \nНаверное \nСейчас концов не найдешь",
"timestamp": "2026-04-05T11:51:00.903741",
"reply_to": null
},
{
"message_id": 6863,
"user_id": 197957361,
"text": null,
"timestamp": "2026-04-05T11:51:00.877190",
"reply_to": 6262
},
{
"message_id": 6864,
"user_id": 197957361,
"text": null,
"timestamp": "2026-04-05T11:51:00.860105",
"reply_to": 6262
}
]
}

View file

@ -0,0 +1,794 @@
{
"export_date": "2026-04-12T23:00:00.070866",
"chat_id": null,
"period": {
"from": "2026-04-05T23:00:00.063081",
"to": "2026-04-12T23:00:00.070885"
},
"summary": {
"total_users": 39,
"verified_users": 39,
"total_messages": 36
},
"users": [
{
"user_id": 7702466390,
"username": "Ulik8319",
"first_name": "Юлия",
"last_name": null,
"apartment": "287",
"verified": true,
"join_date": "2026-04-12T16:31:55.828411",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 5106504485,
"username": null,
"first_name": "Ирина",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-04-12T10:36:28.448884",
"message_count": 4,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 1353946650,
"username": null,
"first_name": "ГеннадийП",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-04-11T12:03:02.546979",
"message_count": 1,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 7346013939,
"username": null,
"first_name": "Любовь",
"last_name": "Землянская",
"apartment": null,
"verified": true,
"join_date": "2026-04-11T08:03:52.447038",
"message_count": 1,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 458413170,
"username": "MeLnikov_Alesha",
"first_name": "Алексей",
"last_name": "Мельников",
"apartment": null,
"verified": true,
"join_date": "2026-04-09T14:09:47.597586",
"message_count": 5,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 5235580033,
"username": null,
"first_name": "Александр",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-04-07T15:59:11.741889",
"message_count": 1,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 772234481,
"username": null,
"first_name": "Анастасия",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-04-05T14:01:56.745028",
"message_count": 3,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 1476737008,
"username": null,
"first_name": "Елена",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-28T17:29:19.435202",
"message_count": 2,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 5036016769,
"username": null,
"first_name": "Алсу",
"last_name": "Юсупова",
"apartment": null,
"verified": true,
"join_date": "2026-03-27T16:54:14.815446",
"message_count": 2,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 864147545,
"username": "roman_garipov",
"first_name": "Roman",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-25T16:59:41.548812",
"message_count": 2,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 6654418281,
"username": "snez_frolova99",
"first_name": "Твоя ногтевая фея💅🏼",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-24T15:02:19.685046",
"message_count": 1,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 2044370304,
"username": "Brasletvz73",
"first_name": "Браслет",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-24T14:50:37.799662",
"message_count": 4,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 5183810334,
"username": null,
"first_name": "Леонид",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-21T21:27:30.494437",
"message_count": 3,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 5167606723,
"username": null,
"first_name": "Tatiyana",
"last_name": null,
"apartment": "297",
"verified": true,
"join_date": "2026-03-21T09:19:51.165950",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1667052403,
"username": null,
"first_name": "Ольга",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-20T11:47:26.400692",
"message_count": 12,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 5166131650,
"username": null,
"first_name": "Николай",
"last_name": "Петрович",
"apartment": null,
"verified": true,
"join_date": "2026-03-20T11:35:37.951913",
"message_count": 2,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 6344554911,
"username": null,
"first_name": "Наташа 🌹",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-19T12:26:06.709415",
"message_count": 3,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 368569899,
"username": "Countesss",
"first_name": "Анастасия",
"last_name": "Лютерова",
"apartment": null,
"verified": true,
"join_date": "2026-03-19T06:07:27.815681",
"message_count": 2,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 7339698511,
"username": null,
"first_name": "Елена",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-19T05:28:20.549649",
"message_count": 3,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 7193750921,
"username": null,
"first_name": "Светлана",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-17T14:10:20.804365",
"message_count": 2,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 5246728667,
"username": "valeriya_nagini",
"first_name": "Валерия",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-17T11:44:28.288584",
"message_count": 2,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1670557872,
"username": "KhaykinaAnastasia",
"first_name": "Анастасия",
"last_name": null,
"apartment": "295",
"verified": true,
"join_date": "2026-03-17T11:44:28.284381",
"message_count": 6,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 2139751123,
"username": null,
"first_name": "Евгений",
"last_name": "Петрович",
"apartment": null,
"verified": true,
"join_date": "2026-03-17T11:44:28.265579",
"message_count": 2,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 1861008160,
"username": null,
"first_name": "Елена",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-17T11:44:28.262810",
"message_count": 1,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 1179832004,
"username": "st5ksu",
"first_name": "Kseniya",
"last_name": "Stepanova",
"apartment": null,
"verified": true,
"join_date": "2026-03-17T11:44:28.259098",
"message_count": 5,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1432214116,
"username": "pro100beze",
"first_name": "Алёна",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-17T11:44:28.256800",
"message_count": 1,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 861072224,
"username": "AFadeeva",
"first_name": "Александра🌤",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-17T11:44:28.228806",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 259925158,
"username": "Izuma_net",
"first_name": "izuma_net",
"last_name": null,
"apartment": "295",
"verified": true,
"join_date": "2026-03-11T10:34:58.022583",
"message_count": 2,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 462572897,
"username": null,
"first_name": "Алекс",
"last_name": null,
"apartment": "48",
"verified": true,
"join_date": "2026-03-10T16:30:06.663941",
"message_count": 3,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 576801733,
"username": "VolkovaElena73",
"first_name": "Елена",
"last_name": "Волкова Ульяновск",
"apartment": null,
"verified": true,
"join_date": "2026-03-10T16:06:40.293427",
"message_count": 5,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1136316705,
"username": "IrinaTs15",
"first_name": "Irina",
"last_name": "Tsyganova",
"apartment": "319",
"verified": true,
"join_date": "2026-03-10T16:06:40.292007",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1970561536,
"username": null,
"first_name": "Окся",
"last_name": null,
"apartment": "255 КВ",
"verified": true,
"join_date": "2026-03-10T16:06:40.289231",
"message_count": 1,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 1299551170,
"username": "katt_rin9",
"first_name": "Екатерина🐈",
"last_name": null,
"apartment": "205",
"verified": true,
"join_date": "2026-03-05T03:59:57.232963",
"message_count": 1,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 1335945859,
"username": null,
"first_name": "O4aPoBaHHblu_cTpaHHuK",
"last_name": null,
"apartment": "",
"verified": true,
"join_date": "2026-03-05T03:59:57.231023",
"message_count": 3,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 1867403697,
"username": "cvetok_nura",
"first_name": "Светлана",
"last_name": null,
"apartment": "171",
"verified": true,
"join_date": "2026-03-05T03:59:57.228965",
"message_count": 4,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 8181748817,
"username": null,
"first_name": "Фаина",
"last_name": null,
"apartment": "186",
"verified": true,
"join_date": "2026-03-05T03:59:57.226876",
"message_count": 14,
"spy_score": 25,
"spy_flags": [
"no_avatar",
"generic_username"
]
},
{
"user_id": 8725618164,
"username": "lkmthreeseven_bot",
"first_name": "DomovoyKontrolBot",
"last_name": null,
"apartment": null,
"verified": true,
"join_date": "2026-03-05T03:59:57.221608",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 5475262081,
"username": null,
"first_name": "Надежда",
"last_name": "Тырина",
"apartment": "152",
"verified": true,
"join_date": "2026-02-23T16:46:55.844916",
"message_count": 0,
"spy_score": 0,
"spy_flags": []
},
{
"user_id": 197957361,
"username": "ktybdsq",
"first_name": "Александр",
"last_name": "MatrixHasYou",
"apartment": "152",
"verified": true,
"join_date": "2026-02-23T13:28:08.090899",
"message_count": 35,
"spy_score": 0,
"spy_flags": []
}
],
"messages": [
{
"message_id": 6929,
"user_id": 259925158,
"text": null,
"timestamp": "2026-04-12T17:50:38.219022",
"reply_to": 6262
},
{
"message_id": 6928,
"user_id": 197957361,
"text": "Погоды не позволяют пока что беседы неспешные во дворе проводить \nНо вот за неделю с несколькими жильцами я тут повстречался, минут по сорок-часу побеседовали\n\nГлаза квадратные у людей от происходящего, шашками хотят махать, писать и прочее\n\nВот уж где инициативная группа поработает весною теплою ! Вот где люди прозреют",
"timestamp": "2026-04-12T15:14:13.060243",
"reply_to": null
},
{
"message_id": 6927,
"user_id": 1667052403,
"text": "Спасибо Александр",
"timestamp": "2026-04-12T15:12:27.802314",
"reply_to": 6926
},
{
"message_id": 6926,
"user_id": 197957361,
"text": "📌 Мы тут с инициативными жильцами обсуждали буквально на днях, что пора бы подавать в суд, иском прям, на нашу \"любимую\" управляющую организацию УО ЖСС, и ее \"опытного бухгалтера\" по совместительству председателя текущего нашего дома - гражданку Ерееву Т.М. \nИ знаете, таки иск в суд отправлен ! \nНу а раз формальные правила требуют от нас отправки копии иска - в адрес Ответчика (УО ЖСС), то им оно ушло первым делом. \n\nТеперь в понедельник и УО ЖСС, и суд заволжский, получат иски\nГде черным по бел",
"timestamp": "2026-04-12T14:20:35.418521",
"reply_to": null
},
{
"message_id": 6925,
"user_id": 197957361,
"text": null,
"timestamp": "2026-04-12T14:14:26.193642",
"reply_to": null
},
{
"message_id": 6924,
"user_id": 197957361,
"text": null,
"timestamp": "2026-04-12T14:14:15.163391",
"reply_to": null
},
{
"message_id": 6923,
"user_id": 197957361,
"text": "Не пойму никак, яйца то при чем - вроде день Гагарина сегодня ? не ? \nВпрочем это лирика ! \nА что у нас по гамбургскому счету ?",
"timestamp": "2026-04-12T14:13:52.405574",
"reply_to": null
},
{
"message_id": 6922,
"user_id": 462572897,
"text": "Не воспитанные, родители взрастили вседозволенность. Хоть праздник, хоть что ... Дикость.",
"timestamp": "2026-04-12T14:05:15.606018",
"reply_to": null
},
{
"message_id": 6918,
"user_id": 5106504485,
"text": "Это они не ели,а раскидали на три этажа",
"timestamp": "2026-04-12T10:39:51.117025",
"reply_to": null
},
{
"message_id": 6917,
"user_id": 1970561536,
"text": "Детки голодные,ели сразу",
"timestamp": "2026-04-12T10:38:08.866439",
"reply_to": null
},
{
"message_id": 6916,
"user_id": 5106504485,
"text": "1подъезд🫣",
"timestamp": "2026-04-12T10:36:53.349640",
"reply_to": null
},
{
"message_id": 6915,
"user_id": 5106504485,
"text": null,
"timestamp": "2026-04-12T10:36:50.775138",
"reply_to": null
},
{
"message_id": 6914,
"user_id": 5106504485,
"text": null,
"timestamp": "2026-04-12T10:36:28.845060",
"reply_to": null
},
{
"message_id": 6913,
"user_id": 1670557872,
"text": null,
"timestamp": "2026-04-12T08:04:05.021389",
"reply_to": null
},
{
"message_id": 6912,
"user_id": 576801733,
"text": "ВОИСТИНУ ВОСКРЕСЕ!!!!!🙏🙏💐🌷🌹",
"timestamp": "2026-04-12T07:41:00.515836",
"reply_to": null
},
{
"message_id": 6911,
"user_id": 8181748817,
"text": "🙏🙏🌞💐",
"timestamp": "2026-04-12T07:16:38.400835",
"reply_to": null
},
{
"message_id": 6911,
"user_id": 8181748817,
"text": "🙏🙏🌞💐",
"timestamp": "2026-04-12T07:16:38.356136",
"reply_to": null
},
{
"message_id": 6910,
"user_id": 5183810334,
"text": null,
"timestamp": "2026-04-12T06:57:10.820177",
"reply_to": null
},
{
"message_id": 6909,
"user_id": 1667052403,
"text": null,
"timestamp": "2026-04-12T06:49:15.228600",
"reply_to": null
},
{
"message_id": 6908,
"user_id": 2139751123,
"text": "ВО ИСТИНУ ВОСКРЕСЕ!!!\nХРИСТОС ВОСКРЕСЕ!!!\nХРИСТОС ВОСКРЕСЕ!!!\nХРИСТОС ВОСКРЕСЕ!!!",
"timestamp": "2026-04-12T06:32:17.398539",
"reply_to": 6907
},
{
"message_id": 6907,
"user_id": 5183810334,
"text": null,
"timestamp": "2026-04-12T04:35:40.077555",
"reply_to": null
},
{
"message_id": 6906,
"user_id": 1353946650,
"text": null,
"timestamp": "2026-04-11T12:03:02.956710",
"reply_to": null
},
{
"message_id": 6905,
"user_id": 7346013939,
"text": "Добрый день! Как наши успехи, деньги собрали на пошлину, теперь проблема с телегой. Информации мало.",
"timestamp": "2026-04-11T08:03:52.949041",
"reply_to": null
},
{
"message_id": 6904,
"user_id": 1335945859,
"text": "Раньше я боялся темноты, но вчера принесли квитанции. Теперь я боюсь света, воды и тепла. И еще немного — мусора.",
"timestamp": "2026-04-10T15:16:15.029712",
"reply_to": 6262
},
{
"message_id": 6903,
"user_id": 8181748817,
"text": "В этой платежке указано что платеж за кап.ремонт за март 26г. (при цене 15,05 руб за кв.м и площади квартиры 66,8 м.кв). \nОплата проводится ежемесячно через РИЦ на спецсчет дома.\nВсе, больше ничего по кап.ремонту платить не нужно.\n\nНикаких прямых платежей в фонд кап.ремонта не должно быть. Счет на 4820 руб - возможно от мошеников, будьте аккуратнее, никаких действий не производите. \n\nОбратитесь в банк, от кого выставлен счет, там выявят, если от мошеников.",
"timestamp": "2026-04-09T19:32:58.749051",
"reply_to": null
},
{
"message_id": 6902,
"user_id": 8181748817,
"text": "В этой платежке указано что платеж за кап.ремонт за март 26г. (при цене 15,05 руб за кв.м и площади квартиры 66,8 м.кв). \nОплата проводится ежемесячно через РИЦ на спецсчет дома.\nВсе, больше ничего по кап.ремонту платить не нужно.\n\nНикаких прямых платежей в фонд кап.ремонта не должно быть. Счет на 4820 руб - возможно от мошеников, будьте аккуратнее, никаких действий не производите. \n\nОбратитесь в банк, от кого выставлен счет, там выявят, если от мошеников.",
"timestamp": "2026-04-09T19:27:30.222466",
"reply_to": 1093
},
{
"message_id": 6901,
"user_id": 458413170,
"text": null,
"timestamp": "2026-04-09T14:55:33.063509",
"reply_to": null
},
{
"message_id": 6900,
"user_id": 458413170,
"text": "Я так понимаю, оплачиваем все",
"timestamp": "2026-04-09T14:55:28.518224",
"reply_to": null
},
{
"message_id": 6899,
"user_id": 458413170,
"text": "Так за капремонт разве мы не оплачиваем ежемесячно в РИЦ?",
"timestamp": "2026-04-09T14:20:25.906081",
"reply_to": null
},
{
"message_id": 6898,
"user_id": 462572897,
"text": "Возможно это капремонт. За несколько месяцев, если не платили ...",
"timestamp": "2026-04-09T14:14:34.659087",
"reply_to": null
},
{
"message_id": 6897,
"user_id": 458413170,
"text": null,
"timestamp": "2026-04-09T14:10:04.284659",
"reply_to": null
},
{
"message_id": 6896,
"user_id": 458413170,
"text": null,
"timestamp": "2026-04-09T14:10:04.260985",
"reply_to": null
},
{
"message_id": 6895,
"user_id": 458413170,
"text": "Добрый день. Пришел счёт в приложение банка. Скажите, это что вообще",
"timestamp": "2026-04-09T14:09:47.993980",
"reply_to": null
},
{
"message_id": 6894,
"user_id": 5246728667,
"text": "Здравствуйте, а в чем проблема вторую ночь подряд нет напора холодной воды? Днем всё хорошо, а ночью холодная вода еле еле течёт.",
"timestamp": "2026-04-07T21:13:03.405086",
"reply_to": null
},
{
"message_id": 6893,
"user_id": 5235580033,
"text": "Щенка никто не терял 6 подъезд, предположительно хаски",
"timestamp": "2026-04-07T15:59:12.269686",
"reply_to": null
},
{
"message_id": 6892,
"user_id": 1179832004,
"text": "Доброе утро,9 подъезд 3-ка нет воды,что случилось?",
"timestamp": "2026-04-06T05:21:23.714970",
"reply_to": null
}
]
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -60,18 +60,23 @@ logging.basicConfig(
]
)
import os
logger = logging.getLogger(__name__)
# МАРКЕР ЗАПУСКА БОТА
logger.info(f"!!! [STARTUP] main.py execution started. PID: {os.getpid()}")
async def on_startup(bot: Bot):
"""Действия при запуске"""
logger.info(f"!!! [STARTUP] on_startup triggered. PID: {os.getpid()}")
logger.info('🚀 LKM37 CORE v4.0 запускается...')
await init_db()
scheduler = Scheduler(bot)
scheduler.start()
web_server = WebServerService(port=8000)
web_server.start()
# Веб-сервер теперь запускается отдельным сервисом (domovoy-web.service)
# web_server = WebServerService(port=8000)
# web_server.start()
try:
await bot.send_message(

View file

@ -3,7 +3,7 @@
"""
import json
import logging
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from typing import Dict, List, Any
from pathlib import Path
from sqlalchemy import select, func, distinct
@ -21,9 +21,10 @@ class ChatAnalytics:
self.session = session
self.export_dir = Path(EXPORT_DIR)
self.export_dir.mkdir(parents=True, exist_ok=True)
self.uly_tz = timezone(timedelta(hours=4))
async def get_general_stats(self) -> Dict[str, Any]:
"""Общая статистика чата"""
"""Общая статистика чата (Оригинальная логика из Gitea)"""
# Всего пользователей
stmt = select(func.count(User.user_id))
result = await self.session.execute(stmt)
@ -41,56 +42,34 @@ class ChatAnalytics:
# Сообщения за неделю
week_ago = datetime.utcnow() - timedelta(days=7)
stmt = (
select(func.count(Message.id))
.where(Message.timestamp >= week_ago)
)
stmt = select(func.count(Message.id)).where(Message.timestamp >= week_ago)
result = await self.session.execute(stmt)
messages_week = result.scalar() or 0
# Сообщения за день
day_ago = datetime.utcnow() - timedelta(days=1)
stmt = (
select(func.count(Message.id))
.where(Message.timestamp >= day_ago)
)
stmt = select(func.count(Message.id)).where(Message.timestamp >= day_ago)
result = await self.session.execute(stmt)
messages_day = result.scalar() or 0
# Подозрительные
stmt = (
select(func.count(User.user_id))
.where(User.spy_score >= 31)
.where(User.spy_score <= 60)
)
result = await self.session.execute(stmt)
suspicious_count = result.scalar() or 0
# Подозрительные и шпионы
stmt_susp = select(func.count(User.user_id)).where(User.spy_score >= 31).where(User.spy_score <= 60)
suspicious_count = (await self.session.execute(stmt_susp)).scalar() or 0
# Шпионы
stmt = (
select(func.count(User.user_id))
.where(User.spy_score > 60)
)
result = await self.session.execute(stmt)
spies_count = result.scalar() or 0
stmt_spies = select(func.count(User.user_id)).where(User.spy_score > 60)
spies_count = (await self.session.execute(stmt_spies)).scalar() or 0
# Топ пользователей за неделю
top_users = await self._get_top_users(week_ago, limit=5)
return {
'total_users': total_users,
'verified_users': verified_users,
'unverified_users': total_users - verified_users,
'total_messages': total_messages,
'messages_week': messages_week,
'messages_day': messages_day,
'suspicious_count': suspicious_count,
'spies_count': spies_count,
'top_users': top_users,
'total_users': total_users, 'verified_users': verified_users,
'unverified_users': total_users - verified_users, 'total_messages': total_messages,
'messages_week': messages_week, 'messages_day': messages_day,
'suspicious_count': suspicious_count, 'spies_count': spies_count, 'top_users': top_users,
}
async def _get_top_users(self, since: datetime, limit: int = 5) -> str:
"""Топ пользователей по активности"""
stmt = (
select(User.user_id, User.first_name, func.count(Message.id).label('count'))
.join(Message, User.user_id == Message.user_id)
@ -101,117 +80,49 @@ class ChatAnalytics:
)
result = await self.session.execute(stmt)
rows = result.all()
if not rows: return 'нет данных'
return ', '.join([f'{i}. {r[1] or f"user{r[0]}"}: {r[2]}' for i, r in enumerate(rows, 1)])
if not rows:
return 'нет данных'
lines = []
for i, (user_id, name, count) in enumerate(rows, 1):
display_name = name or f'user{user_id}'
lines.append(f'{i}. {display_name}: {count}')
return ', '.join(lines)
async def export_to_json(self) -> str:
async def export_to_json(self, full_history: bool = False) -> str:
"""
Экспорт данных чата в JSON
Возвращает путь к файлу
Экспорт в JSON.
full_history=False -> за неделю (для планировщика)
full_history=True -> всё (для кнопки в меню)
"""
timestamp = datetime.utcnow().strftime('%Y%m%d_%H%M%S')
filename = f'chat_export_{timestamp}.json'
now_local = datetime.now(self.uly_tz)
prefix = "full_" if full_history else "weekly_"
filename = f'chat_export_{prefix}{now_local.strftime("%Y%m%d_%H%M%S")}.json'
filepath = self.export_dir / filename
# Получаем всех пользователей
stmt = select(User).order_by(User.join_date.desc())
result = await self.session.execute(stmt)
users = list(result.scalars().all())
# Пользователи
users = list((await self.session.execute(select(User).order_by(User.user_id))).scalars().all())
# Получаем сообщения за последнюю неделю
# Сообщения
stmt = select(Message).order_by(Message.timestamp.asc())
if not full_history:
week_ago = datetime.utcnow() - timedelta(days=7)
stmt = (
select(Message)
.where(Message.timestamp >= week_ago)
.order_by(Message.timestamp.desc())
.limit(10000) # Ограничение на количество
)
result = await self.session.execute(stmt)
messages = list(result.scalars().all())
stmt = stmt.where(Message.timestamp >= week_ago)
messages = list((await self.session.execute(stmt)).scalars().all())
# Формируем экспорт
export_data = {
'export_date': datetime.utcnow().isoformat(),
'chat_id': None, # Заполнится при использовании
'period': {
'from': week_ago.isoformat(),
'to': datetime.utcnow().isoformat(),
},
'export_date': now_local.isoformat(),
'is_full_history': full_history,
'summary': {
'total_users': len(users),
'verified_users': sum(1 for u in users if u.verified),
'total_messages': len(messages),
},
'users': [
{
'user_id': u.user_id,
'username': u.username,
'first_name': u.first_name,
'last_name': u.last_name,
'apartment': u.apartment,
'verified': u.verified,
'join_date': u.join_date.isoformat() if u.join_date else None,
'message_count': u.message_count,
'spy_score': u.spy_score,
'spy_flags': json.loads(u.spy_flags) if u.spy_flags else [],
}
for u in users
],
'messages': [
{
'message_id': m.message_id,
'user_id': m.user_id,
'text': m.text[:500] if m.text else None, # Обрезаем длинные
'timestamp': m.timestamp.isoformat() if m.timestamp else None,
'reply_to': m.reply_to_message_id,
}
for m in messages
]
'users': [{
'user_id': u.user_id, 'username': u.username, 'first_name': u.first_name,
'apartment': u.apartment, 'verified': u.verified, 'rating': u.rating
} for u in users],
'messages': [{
'id': m.id, 'user_id': m.user_id, 'text': m.text,
'date': m.timestamp.isoformat() if m.timestamp else None, 'topic': m.topic
} for m in messages]
}
# Сохраняем
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(export_data, f, ensure_ascii=False, indent=2)
logger.info(f'Экспорт сохранён: {filepath}')
return str(filepath)
async def get_activity_report(self, days: int = 7) -> Dict[str, Any]:
"""Отчёт по активности за период"""
since = datetime.utcnow() - timedelta(days=days)
# Активные пользователи
stmt = (
select(distinct(Message.user_id))
.where(Message.timestamp >= since)
)
result = await self.session.execute(stmt)
active_users = len(list(result.scalars().all()))
# Самые активные часы
stmt = (
select(
func.strftime('%H', Message.timestamp).label('hour'),
func.count().label('count')
)
.where(Message.timestamp >= since)
.group_by(func.strftime('%H', Message.timestamp))
.order_by(func.count().desc())
.limit(5)
)
result = await self.session.execute(stmt)
peak_hours = [(row.hour, row.count) for row in result.all()]
return {
'active_users': active_users,
'peak_hours': peak_hours,
'period_days': days,
}

View file

@ -0,0 +1,217 @@
"""
Аналитика чата и экспорт данных
"""
import json
import logging
from datetime import datetime, timedelta
from typing import Dict, List, Any
from pathlib import Path
from sqlalchemy import select, func, distinct
from sqlalchemy.ext.asyncio import AsyncSession
from database.models import User, Message
from config import EXPORT_DIR
logger = logging.getLogger(__name__)
class ChatAnalytics:
"""Аналитика чата"""
def __init__(self, session: AsyncSession):
self.session = session
self.export_dir = Path(EXPORT_DIR)
self.export_dir.mkdir(parents=True, exist_ok=True)
async def get_general_stats(self) -> Dict[str, Any]:
"""Общая статистика чата"""
# Всего пользователей
stmt = select(func.count(User.user_id))
result = await self.session.execute(stmt)
total_users = result.scalar() or 0
# Верифицированы
stmt = select(func.count(User.user_id)).where(User.verified == True)
result = await self.session.execute(stmt)
verified_users = result.scalar() or 0
# Сообщения всего
stmt = select(func.count(Message.id))
result = await self.session.execute(stmt)
total_messages = result.scalar() or 0
# Сообщения за неделю
week_ago = datetime.utcnow() - timedelta(days=7)
stmt = (
select(func.count(Message.id))
.where(Message.timestamp >= week_ago)
)
result = await self.session.execute(stmt)
messages_week = result.scalar() or 0
# Сообщения за день
day_ago = datetime.utcnow() - timedelta(days=1)
stmt = (
select(func.count(Message.id))
.where(Message.timestamp >= day_ago)
)
result = await self.session.execute(stmt)
messages_day = result.scalar() or 0
# Подозрительные
stmt = (
select(func.count(User.user_id))
.where(User.spy_score >= 31)
.where(User.spy_score <= 60)
)
result = await self.session.execute(stmt)
suspicious_count = result.scalar() or 0
# Шпионы
stmt = (
select(func.count(User.user_id))
.where(User.spy_score > 60)
)
result = await self.session.execute(stmt)
spies_count = result.scalar() or 0
# Топ пользователей за неделю
top_users = await self._get_top_users(week_ago, limit=5)
return {
'total_users': total_users,
'verified_users': verified_users,
'unverified_users': total_users - verified_users,
'total_messages': total_messages,
'messages_week': messages_week,
'messages_day': messages_day,
'suspicious_count': suspicious_count,
'spies_count': spies_count,
'top_users': top_users,
}
async def _get_top_users(self, since: datetime, limit: int = 5) -> str:
"""Топ пользователей по активности"""
stmt = (
select(User.user_id, User.first_name, func.count(Message.id).label('count'))
.join(Message, User.user_id == Message.user_id)
.where(Message.timestamp >= since)
.group_by(User.user_id, User.first_name)
.order_by(func.count(Message.id).desc())
.limit(limit)
)
result = await self.session.execute(stmt)
rows = result.all()
if not rows:
return 'нет данных'
lines = []
for i, (user_id, name, count) in enumerate(rows, 1):
display_name = name or f'user{user_id}'
lines.append(f'{i}. {display_name}: {count}')
return ', '.join(lines)
async def export_to_json(self) -> str:
"""
Экспорт данных чата в JSON
Возвращает путь к файлу
"""
timestamp = datetime.utcnow().strftime('%Y%m%d_%H%M%S')
filename = f'chat_export_{timestamp}.json'
filepath = self.export_dir / filename
# Получаем всех пользователей
stmt = select(User).order_by(User.join_date.desc())
result = await self.session.execute(stmt)
users = list(result.scalars().all())
# Получаем сообщения за последнюю неделю
week_ago = datetime.utcnow() - timedelta(days=7)
stmt = (
select(Message)
.where(Message.timestamp >= week_ago)
.order_by(Message.timestamp.desc())
.limit(10000) # Ограничение на количество
)
result = await self.session.execute(stmt)
messages = list(result.scalars().all())
# Формируем экспорт
export_data = {
'export_date': datetime.utcnow().isoformat(),
'chat_id': None, # Заполнится при использовании
'period': {
'from': week_ago.isoformat(),
'to': datetime.utcnow().isoformat(),
},
'summary': {
'total_users': len(users),
'verified_users': sum(1 for u in users if u.verified),
'total_messages': len(messages),
},
'users': [
{
'user_id': u.user_id,
'username': u.username,
'first_name': u.first_name,
'last_name': u.last_name,
'apartment': u.apartment,
'verified': u.verified,
'join_date': u.join_date.isoformat() if u.join_date else None,
'message_count': u.message_count,
'spy_score': u.spy_score,
'spy_flags': json.loads(u.spy_flags) if u.spy_flags else [],
}
for u in users
],
'messages': [
{
'message_id': m.message_id,
'user_id': m.user_id,
'text': m.text[:500] if m.text else None, # Обрезаем длинные
'timestamp': m.timestamp.isoformat() if m.timestamp else None,
'reply_to': m.reply_to_message_id,
}
for m in messages
]
}
# Сохраняем
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(export_data, f, ensure_ascii=False, indent=2)
logger.info(f'Экспорт сохранён: {filepath}')
return str(filepath)
async def get_activity_report(self, days: int = 7) -> Dict[str, Any]:
"""Отчёт по активности за период"""
since = datetime.utcnow() - timedelta(days=days)
# Активные пользователи
stmt = (
select(distinct(Message.user_id))
.where(Message.timestamp >= since)
)
result = await self.session.execute(stmt)
active_users = len(list(result.scalars().all()))
# Самые активные часы
stmt = (
select(
func.strftime('%H', Message.timestamp).label('hour'),
func.count().label('count')
)
.where(Message.timestamp >= since)
.group_by(func.strftime('%H', Message.timestamp))
.order_by(func.count().desc())
.limit(5)
)
result = await self.session.execute(stmt)
peak_hours = [(row.hour, row.count) for row in result.all()]
return {
'active_users': active_users,
'peak_hours': peak_hours,
'period_days': days,
}

119
services/chat_exporter.py Normal file
View file

@ -0,0 +1,119 @@
"""
Надёжный сервис экспорта всей истории чата в JSON и Git.
"""
import json
import logging
import subprocess
import os
from pathlib import Path
from datetime import datetime, timezone, timedelta
from sqlalchemy import select, func
from database.models import User, Message as DBMessage
logger = logging.getLogger("ChatExporter")
class ChatExporter:
def __init__(self, session):
self.session = session
self.base_dir = Path(__file__).parent.parent
self.export_dir = self.base_dir / "exports"
self.export_dir.mkdir(exist_ok=True)
self.uly_tz = timezone(timedelta(hours=4))
async def run_full_export(self):
"""Полный цикл: Генерация -> Запись в БД -> Push в Git"""
try:
now_local = datetime.now(self.uly_tz)
timestamp = now_local.strftime("%Y-%m-%d_%H-%M")
filename = f"chat_history_{timestamp}.json"
filepath = self.export_dir / filename
# 1. Собираем данные
users_res = await self.session.execute(select(User))
users = users_res.scalars().all()
msg_res = await self.session.execute(select(DBMessage).order_by(DBMessage.timestamp.asc()))
messages = msg_res.scalars().all()
export_data = {
"info": {
"exported_at": now_local.isoformat(),
"total_users": len(list(users)),
"total_messages": len(list(messages))
},
"users": [
{
"id": u.user_id, "username": u.username, "name": u.full_name,
"apt": u.apartment, "verified": u.verified
} for u in users
],
"messages": [
{
"id": m.id, "user_id": m.user_id, "text": m.text,
"date": m.timestamp.isoformat() if m.timestamp else None,
"topic": m.topic
} for m in messages
]
}
# 2. Сохраняем файл
with open(filepath, "w", encoding="utf-8") as f:
json.dump(export_data, f, ensure_ascii=False, indent=2)
# 3. Пушим в Gitea
git_success = self._push_to_gitea(filepath)
# 4. Логируем в БД
await self._log_to_db(filename, len(messages), git_success)
return {"success": True, "filename": filename, "messages": len(messages), "git": git_success}
except Exception as e:
logger.error(f"❌ Критическая ошибка экспорта: {e}", exc_info=True)
return {"success": False, "error": str(e)}
def _push_to_gitea(self, filepath):
"""Отправка файла в Gitea через системный git"""
try:
# Напрямую, без прокси
env = os.environ.copy()
for var in ['http_proxy', 'https_proxy', 'all_proxy', 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY']:
env.pop(var, None)
# Переходим в корень проекта для git команд
os.chdir(str(self.base_dir))
subprocess.run(["git", "add", "-f", str(filepath)], check=True, env=env)
subprocess.run(["git", "commit", "-m", f"📦 Авто-экспорт чата: {filepath.name}"], check=True, env=env)
subprocess.run(["git", "push", "origin", "main"], check=True, env=env)
logger.info(f"✅ Файл {filepath.name} успешно отправлен в Gitea")
return True
except Exception as e:
logger.error(f"❌ Ошибка Git Push: {e}")
return False
async def _log_to_db(self, filename, count, git_pushed):
from sqlalchemy import text
try:
# Используем текстовый запрос, так как у нас нет модели для этой таблицы
sql = text("INSERT INTO chat_exports (filename, total_messages, git_pushed) VALUES (:f, :c, :g)")
await self.session.execute(sql, {"f": filename, "c": count, "g": 1 if git_pushed else 0})
await self.session.commit()
except Exception as e:
logger.error(f"❌ Ошибка записи в таблицу экспортов: {e}")
async def get_stats(self):
"""Получить статистику для веб-панели"""
from sqlalchemy import text
try:
sql = text("SELECT COUNT(*), MAX(export_date) FROM chat_exports")
res = await self.session.execute(sql)
row = res.first()
if row:
count, last_date = row
return {"total": count or 0, "last_date": last_date}
return {"total": 0, "last_date": None}
except Exception as e:
logger.error(f"Error getting stats: {e}")
return {"total": 0, "last_date": None}

View file

@ -127,29 +127,31 @@ class Scheduler:
logger.info('🛑 Планировщик остановлен')
async def weekly_export(self):
"""Еженедельный экспорт данных"""
"""Еженедельный экспорт данных (Новая логика v2026)"""
try:
from database.db import AsyncSessionLocal
from services.chat_exporter import ChatExporter
async with AsyncSessionLocal() as session:
analytics = ChatAnalytics(session)
filepath = await analytics.export_to_json()
exporter = ChatExporter(session)
result = await exporter.run_full_export()
status_git = "В Gitea" if result.get('git') else "❌ Ошибка Gitea"
await self.bot.send_message(
ADMIN_USER_ID,
f'📥 <b>Еженедельный экспорт выполнен</b>\n\n'
f'Файл сохранён: {filepath}\n'
f'Время: {datetime.utcnow().strftime("%d.%m.%Y %H:%M")}'
f'📥 <b>Еженедельный экспорт завершён</b>\n\n'
f'📄 Файл: <code>{result.get("filename")}</code>\n'
f'📊 Сообщений: {result.get("messages")}\n'
f'🌐 Статус: {status_git}\n'
f'🕒 Пояс: Ульяновск (UTC+4)'
)
logger.info(f'Еженедельный экспорт: {filepath}')
logger.info(f"✅ Weekly export finished: {result.get('filename')}")
except Exception as e:
logger.error(f'Ошибка экспорта: {e}')
await self.bot.send_message(
ADMIN_USER_ID,
f'❌ <b>Ошибка экспорта</b>\n\n{str(e)}'
)
await self.bot.send_message(ADMIN_USER_ID, f'❌ <b>Ошибка экспорта</b>\n\n{str(e)}')
async def daily_spy_check(self):
"""Ежедневная проверка на шпионов"""

6
test_web.py Normal file
View file

@ -0,0 +1,6 @@
from fastapi import FastAPI
import uvicorn
app = FastAPI()
@app.get("/")
def read_root(): return {"Hello": "World"}
if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8005)

View file

@ -1,13 +1,30 @@
"""
Web Admin Interface v4.2 REBORN - MEGA ADMIN PANEL
Полноценное управление ВСЕМ функционалом бота
Web Admin Interface v4.5 REBORN - MEGA ADMIN PANEL
Полноценное управление ВСЕМ функционалом бота:
- Dashboard, Users, Verification, Phones, Ads, Polls, Events, Schedules
- Smart Broadcasts (с отслеживанием прочтения)
- Scheduled Posts (с фото/документами)
- Weekly Digests (v3.1)
- Chat Exporter (JSON + Gitea Push)
- Email Auditor (аудит переписки)
"""
import logging
import json
import aiohttp
import re
import os
import sys
import secrets
import subprocess
from pathlib import Path
from fastapi import FastAPI, Request, Depends, HTTPException, status, Form, File, UploadFile
from datetime import datetime, timedelta, timezone
# ГАРАНТИРУЕМ, что корень проекта в пути поиска модулей
BASE_DIR = Path(__file__).parent.parent
if str(BASE_DIR) not in sys.path:
sys.path.insert(0, str(BASE_DIR))
from fastapi import FastAPI, Request, Depends, HTTPException, status, Form, File, UploadFile, Query
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
@ -15,34 +32,36 @@ from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, File
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, update, delete, desc, text
from database.models import User, Message, EmailExportLog, EmailAudit
from datetime import datetime, timedelta, timezone
import secrets
import config
from database.db import AsyncSessionLocal
from database.models import (
User, Message as DBMessage, Poll, Ad, PaymentReminder, Schedule,
VerificationRequest, Service, Announcement, Event, ScheduledPost,
Broadcast, BroadcastRead, Digest, InitiativeGroup, EmailAudit, EmailExportLog,
ProfileHistory, Achievement, Thank, SpyLog
)
# ===== ЧАСОВОЙ ПОЯС УЛЬЯНОВСК (UTC+4) =====
ULY_TZ = timezone(timedelta(hours=4))
def to_local(dt: datetime) -> datetime:
def to_local(dt) -> datetime:
if dt is None: return None
# Если время без часового пояса (naive), считаем что оно в UTC (как в БД)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
if isinstance(dt, str):
try:
dt = datetime.fromisoformat(dt.replace(' ', 'T'))
except: return None
if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc)
return dt.astimezone(ULY_TZ)
def fmt_local(dt: datetime, fmt: str = '%d.%m.%Y %H:%M') -> str:
def fmt_local(dt, fmt: str = '%d.%m.%Y %H:%M') -> str:
local_dt = to_local(dt)
return local_dt.strftime(fmt) if local_dt else '-'
from database.db import AsyncSessionLocal
from database.models import (
User, Message, Poll, Ad, PaymentReminder, Schedule,
VerificationRequest, Service, Announcement, Event, ScheduledPost,
Broadcast, BroadcastRead, Digest, InitiativeGroup, EmailAudit, EmailExportLog
)
logger = logging.getLogger(__name__)
app = FastAPI(title="LKM37 MEGA Admin Panel", version="4.2.0")
logger.info(f"!!! [STARTUP] Unified Web App loaded. PID: {os.getpid()}")
app = FastAPI(title="LKM37 MEGA Admin Panel", version="4.5.0")
app.add_middleware(
CORSMiddleware,
@ -52,27 +71,26 @@ app.add_middleware(
allow_headers=["*"],
)
@app.exception_handler(Exception)
async def debug_exception_handler(request: Request, exc: Exception):
logger.error(f"❌ WEB ERROR: {exc}", exc_info=True)
return JSONResponse(status_code=500, content={"message": f"Internal Server Error: {str(exc)}"})
templates = Jinja2Templates(directory=str(BASE_DIR / "web" / "templates"))
app.mount("/static", StaticFiles(directory=str(BASE_DIR / "web" / "static")), name="static")
app.mount("/static/service_images", StaticFiles(directory=str(BASE_DIR / "data" / "service_images")), name="service_images")
app.mount("/static/email_archive", StaticFiles(directory=str(BASE_DIR / "services" / "email_auditor" / "exported_emails")), name="email_archive")
templates = Jinja2Templates(directory="web/templates")
base_dir = Path(__file__).parent.parent
app.mount("/static", StaticFiles(directory="web/static"), name="static")
app.mount("/static/service_images", StaticFiles(directory=str(base_dir / "data" / "service_images")), name="service_images")
app.mount("/static/email_archive", StaticFiles(directory=str(base_dir / "services" / "email_auditor" / "exported_emails")), name="email_archive")
security = HTTPBasic()
def get_current_admin(credentials: HTTPBasicCredentials = Depends(security)):
correct_username = secrets.compare_digest(credentials.username, config.WEB_ADMIN_LOGIN)
correct_password = secrets.compare_digest(credentials.password, config.WEB_ADMIN_PASSWORD)
if not (correct_username and correct_password):
raise HTTPException(status_code=401, detail="Неверный логин или пароль", headers={"WWW-Authenticate": "Basic"})
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect login or password",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
# ============================================================================
# ГЛАВНАЯ
# ГЛАВНАЯ (DASHBOARD)
# ============================================================================
@app.get("/", response_class=HTMLResponse)
@ -80,20 +98,21 @@ async def dashboard(request: Request, username: str = Depends(get_current_admin)
async with AsyncSessionLocal() as session:
users_count = (await session.execute(select(func.count(User.user_id)))).scalar()
verified_count = (await session.execute(select(func.count(User.user_id)).where(User.verified == True))).scalar()
messages_count = (await session.execute(select(func.count(Message.id)))).scalar()
messages_count = (await session.execute(select(func.count(DBMessage.id)))).scalar()
active_ads = (await session.execute(select(func.count(Ad.ad_id)).where(Ad.is_active == True))).scalar()
active_polls = (await session.execute(select(func.count(Poll.poll_id)).where(Poll.is_active == True))).scalar()
upcoming_events = (await session.execute(select(func.count(Event.id)).where(Event.is_active == True))).scalar()
pending_count = (await session.execute(select(func.count(VerificationRequest.id)).where(VerificationRequest.status == 'pending'))).scalar()
return templates.TemplateResponse(
request=request, name="dashboard.html",
context={
"username": username, "users_count": users_count, "verified_count": verified_count,
"messages_count": messages_count, "active_ads": active_ads, "active_polls": active_polls,
"upcoming_events": upcoming_events, "pending_count": pending_count
}
)
return templates.TemplateResponse("dashboard.html", {
"request": request, "username": username,
"stats": {
"users": users_count, "verified": verified_count,
"messages": messages_count, "ads": active_ads,
"polls": active_polls, "events": upcoming_events
},
"pending_count": pending_count
})
# ============================================================================
# ПОЛЬЗОВАТЕЛИ
@ -105,330 +124,316 @@ async def users_page(request: Request, search: str = "", filter: str = "", usern
stmt = select(User)
if filter == 'verified': stmt = stmt.where(User.verified == True)
elif filter == 'unverified': stmt = stmt.where(User.verified == False)
elif filter == 'active_unverified': stmt = stmt.where(User.verified == False, User.message_count >= 50)
elif filter == 'ig':
ig_stmt = select(InitiativeGroup.user_id).where(InitiativeGroup.is_active == True)
ig_user_ids = [row[0] for row in (await session.execute(ig_stmt)).all()]
stmt = stmt.where(User.user_id.in_(ig_user_ids))
if search:
stmt = stmt.where((User.first_name.ilike(f"%{search}%")) | (User.username.ilike(f"%{search}%")) | (User.apartment.ilike(f"%{search}%")))
stmt = stmt.where(
(User.first_name.ilike(f"%{search}%")) |
(User.last_name.ilike(f"%{search}%")) |
(User.username.ilike(f"%{search}%")) |
(User.apartment.ilike(f"%{search}%"))
)
users = list((await session.execute(stmt.order_by(User.user_id.desc()).limit(100))).scalars().all())
total_count = (await session.execute(select(func.count(User.user_id)))).scalar()
verified_count = (await session.execute(select(func.count(User.user_id)).where(User.verified == True))).scalar()
stmt = stmt.order_by(User.user_id.desc()).limit(100)
users = list((await session.execute(stmt)).scalars().all())
ig_stmt = select(InitiativeGroup.user_id).where(InitiativeGroup.is_active == True)
ig_user_ids = set(row[0] for row in (await session.execute(ig_stmt)).all())
pending_stmt = select(VerificationRequest).where(VerificationRequest.status == 'pending')
pending_requests = list((await session.execute(pending_stmt)).scalars().all())
pending_stmt = select(VerificationRequest).where(VerificationRequest.status == 'pending').order_by(VerificationRequest.created_at.desc())
pending_requests = (await session.execute(pending_stmt)).scalars().all()
return templates.TemplateResponse(
request=request, name="users.html",
context={
"username": username, "users": users, "search": search, "current_filter": filter,
"total_count": total_count, "verified_count": verified_count, "unverified_count": total_count - verified_count,
"ig_count": len(ig_user_ids), "ig_user_ids": ig_user_ids, "pending_requests": [], "pending_count": len(pending_requests),
"active_unverified": 0
}
)
total_count = (await session.execute(select(func.count(User.user_id)))).scalar()
verified_count = (await session.execute(select(func.count(User.user_id)).where(User.verified == True))).scalar()
active_unverified = (await session.execute(select(func.count(User.user_id)).where(User.verified == False, User.message_count >= 50))).scalar()
# ============================================================================
# РАССЫЛКИ
# ============================================================================
return templates.TemplateResponse("users.html", {
"request": request, "username": username, "users": users, "search": search, "current_filter": filter,
"stats": {"total": total_count, "verified": verified_count, "unverified": total_count - verified_count, "active_unverified": active_unverified, "ig": len(ig_user_ids)},
"ig_user_ids": ig_user_ids, "pending_count": len(list(pending_requests))
})
@app.get("/broadcast", response_class=HTMLResponse)
async def broadcast_page(request: Request, username: str = Depends(get_current_admin)):
@app.get("/api/user/{user_id}")
async def api_get_user(user_id: int, username: str = Depends(get_current_admin)):
async with AsyncSessionLocal() as session:
broadcasts = list((await session.execute(select(Broadcast).order_by(Broadcast.created_at.desc()).limit(50))).scalars().all())
return templates.TemplateResponse(request=request, name="broadcast.html", context={"username": username, "broadcasts": broadcasts})
user = await session.get(User, user_id)
if not user: return JSONResponse({"error": "Not found"}, status_code=404)
return JSONResponse({"user": {"id": user.user_id, "username": user.username, "first_name": user.first_name, "last_name": user.last_name, "apartment": user.apartment, "phone": user.phone, "verified": user.verified, "rating": user.rating, "message_count": user.message_count, "is_banned": user.is_banned}})
@app.post("/api/broadcast/create")
async def api_create_broadcast(
text: str = Form(...),
photo: UploadFile = File(None),
recipients: str = Form("all_and_chat"),
staircase: str = Form("all"),
username: str = Depends(get_current_admin)
):
from handlers.smart_broadcast import send_smart_broadcast
from bot_instance import get_bot
photo_file_id = None
if photo and photo.filename:
temp_path = Path("data") / photo.filename
with open(temp_path, "wb") as f: f.write(await photo.read())
async with aiohttp.ClientSession() as session:
url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/sendPhoto"
data = aiohttp.FormData()
data.add_field('chat_id', str(config.ADMIN_USER_ID))
data.add_field('photo', open(temp_path, 'rb'))
async with session.post(url, data=data, proxy=config.get_proxy_url()) as resp:
res = await resp.json()
if res.get('ok'): photo_file_id = res['result']['photo'][-1]['file_id']
if temp_path.exists(): temp_path.unlink()
bot = get_bot()
success = await send_smart_broadcast(bot, text, recipients, photo_file_id, staircase)
return JSONResponse({"success": success})
# ============================================================================
# ФАЙЛЫ (МЕНЕДЖЕР)
# ============================================================================
@app.get("/files", response_class=HTMLResponse)
async def files_page(request: Request, username: str = Depends(get_current_admin)):
return templates.TemplateResponse(request=request, name="files.html", context={"username": username})
@app.get("/api/files/list")
async def api_list_files(username: str = Depends(get_current_admin)):
@app.post("/api/user/{user_id}/update")
async def api_update_user(user_id: int, request: Request, username: str = Depends(get_current_admin)):
data = await request.json()
async with AsyncSessionLocal() as session:
result = await session.execute(text("SELECT id, filename, file_id, file_type FROM assets ORDER BY created_at DESC"))
files = [{"id": r[0], "filename": r[1], "file_id": r[2], "file_type": r[3]} for r in result.all()]
return JSONResponse({"success": True, "files": files})
update_data = {}
if 'apartment' in data: update_data['apartment'] = data['apartment'].upper()
if 'phone' in data: update_data['phone'] = data['phone']
if 'verified' in data:
update_data['verified'] = bool(data['verified'])
if data['verified']: update_data['verification_date'] = datetime.utcnow()
if 'rating' in data: update_data['rating'] = int(data['rating'])
if update_data:
await session.execute(update(User).where(User.user_id == user_id).values(**update_data))
await session.commit()
return JSONResponse({"success": True})
@app.post("/api/files/upload")
async def api_upload_file(file: UploadFile = File(...), username: str = Depends(get_current_admin)):
temp_path = Path("data") / file.filename
with open(temp_path, "wb") as f: f.write(await file.read())
file_ext = Path(file.filename).suffix.lower()
is_photo = file_ext in ['.jpg', '.jpeg', '.png', '.webp']
tg_method = "sendPhoto" if is_photo else "sendDocument"
@app.post("/api/user/{user_id}/verify")
async def api_verify_user(user_id: int, username: str = Depends(get_current_admin)):
async with AsyncSessionLocal() as session:
await session.execute(update(User).where(User.user_id == user_id).values(verified=True, verification_date=datetime.utcnow()))
await session.commit()
return JSONResponse({"success": True})
try:
async with aiohttp.ClientSession() as session:
url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/{tg_method}"
data = aiohttp.FormData()
data.add_field('chat_id', str(config.ADMIN_USER_ID))
data.add_field('photo' if is_photo else 'document', open(temp_path, 'rb'))
async with session.post(url, data=data, proxy=config.get_proxy_url()) as resp:
res = await resp.json()
if not res.get('ok'): raise Exception(res.get('description'))
fid = res['result']['photo'][-1]['file_id'] if is_photo else res['result']['document']['file_id']
async with AsyncSessionLocal() as sdb:
await sdb.execute(text("INSERT INTO assets (filename, file_id, file_type) VALUES (:fn, :fid, :ft)"),
{"fn": file.filename, "fid": fid, "ft": "photo" if is_photo else "document"})
await sdb.commit()
return JSONResponse({"success": True, "file_id": fid})
except Exception as e: return JSONResponse({"success": False, "error": str(e)}, status_code=500)
finally:
if temp_path.exists(): temp_path.unlink()
@app.post("/api/user/{user_id}/unverify")
async def api_unverify_user(user_id: int, username: str = Depends(get_current_admin)):
async with AsyncSessionLocal() as session:
await session.execute(update(User).where(User.user_id == user_id).values(verified=False))
await session.commit()
return JSONResponse({"success": True})
# ============================================================================
# ОСТАЛЬНЫЕ РОУТЫ (ЗАГЛУШКИ / БАЗОВЫЕ)
# ВЕРИФИКАЦИЯ
# ============================================================================
@app.get("/verification", response_class=HTMLResponse)
async def verification_page(request: Request, username: str = Depends(get_current_admin)):
async with AsyncSessionLocal() as session:
pending = list((await session.execute(select(VerificationRequest).where(VerificationRequest.status == 'pending'))).scalars().all())
unverified = list((await session.execute(select(User).where(User.verified == False).limit(50))).scalars().all())
return templates.TemplateResponse(request=request, name="verification.html", context={"username": username, "pending_requests": pending, "unverified_users": unverified})
pending = (await session.execute(select(VerificationRequest).where(VerificationRequest.status == 'pending').order_by(VerificationRequest.created_at.desc()))).scalars().all()
unverified = (await session.execute(select(User).where(User.verified == False).order_by(User.join_date.desc()).limit(50))).scalars().all()
return templates.TemplateResponse("verification.html", {"request": request, "username": username, "pending_requests": pending, "unverified_users": unverified})
@app.post("/api/verification/{req_id}/approve")
async def api_approve_verification(req_id: int, username: str = Depends(get_current_admin)):
async with AsyncSessionLocal() as session:
req = (await session.execute(select(VerificationRequest).where(VerificationRequest.id == req_id))).scalar_one_or_none()
if req:
req.status = 'approved'
req.reviewed_at = datetime.utcnow()
await session.execute(update(User).where(User.user_id == req.user_id).values(verified=True, apartment=req.apartment, verification_date=datetime.utcnow()))
await session.commit()
return JSONResponse({"success": True})
@app.post("/api/verification/{req_id}/reject")
async def api_reject_verification(req_id: int, username: str = Depends(get_current_admin)):
async with AsyncSessionLocal() as session:
req = (await session.execute(select(VerificationRequest).where(VerificationRequest.id == req_id))).scalar_one_or_none()
if req:
req.status = 'rejected'
req.reviewed_at = datetime.utcnow()
await session.commit()
return JSONResponse({"success": True})
# ============================================================================
# ТЕЛЕФОНЫ / ОБЪЯВЛЕНИЯ / ОПРОСЫ / СОБЫТИЯ / ГРАФИКИ
# ============================================================================
@app.get("/phones", response_class=HTMLResponse)
async def phones_page(request: Request, username: str = Depends(get_current_admin)):
async with AsyncSessionLocal() as session:
services = list((await session.execute(select(Service).order_by(Service.category, Service.name))).scalars().all())
return templates.TemplateResponse(request=request, name="phones.html", context={"username": username, "services": services})
@app.get("/schedules", response_class=HTMLResponse)
async def schedules_page(request: Request, username: str = Depends(get_current_admin)):
async with AsyncSessionLocal() as session:
schedules = list((await session.execute(select(Schedule).order_by(Schedule.start_time.desc()))).scalars().all())
return templates.TemplateResponse(request=request, name="schedules.html", context={"username": username, "schedules": schedules})
services = (await session.execute(select(Service).order_by(Service.category, Service.name))).scalars().all()
return templates.TemplateResponse("phones.html", {"request": request, "username": username, "services": services})
@app.get("/ads", response_class=HTMLResponse)
async def ads_page(request: Request, username: str = Depends(get_current_admin)):
async with AsyncSessionLocal() as session:
ads = list((await session.execute(select(Ad).order_by(Ad.created_at.desc()).limit(100))).scalars().all())
return templates.TemplateResponse(request=request, name="ads.html", context={"username": username, "ads": ads})
ads = (await session.execute(select(Ad).order_by(Ad.created_at.desc()).limit(100))).scalars().all()
return templates.TemplateResponse("ads.html", {"request": request, "username": username, "ads": ads})
@app.get("/polls", response_class=HTMLResponse)
async def polls_page(request: Request, username: str = Depends(get_current_admin)):
async with AsyncSessionLocal() as session:
polls = list((await session.execute(select(Poll).order_by(Poll.created_at.desc()).limit(50))).scalars().all())
return templates.TemplateResponse(request=request, name="polls.html", context={"username": username, "polls": polls})
polls = (await session.execute(select(Poll).order_by(Poll.created_at.desc()).limit(50))).scalars().all()
return templates.TemplateResponse("polls.html", {"request": request, "username": username, "polls": polls})
@app.get("/events", response_class=HTMLResponse)
async def events_page(request: Request, username: str = Depends(get_current_admin)):
async with AsyncSessionLocal() as session:
events = list((await session.execute(select(Event).order_by(Event.event_date.desc()).limit(50))).scalars().all())
return templates.TemplateResponse(request=request, name="events.html", context={"username": username, "events": events})
events = (await session.execute(select(Event).order_by(Event.event_date.desc()).limit(50))).scalars().all()
return templates.TemplateResponse("events.html", {"request": request, "username": username, "events": events})
@app.get("/digests", response_class=HTMLResponse)
async def digests_page(request: Request, username: str = Depends(get_current_admin)):
return templates.TemplateResponse(request=request, name="digests.html", context={"username": username, "pending_digests": [], "archive_digests": []})
@app.get("/export", response_class=HTMLResponse)
async def export_page(request: Request, username: str = Depends(get_current_admin)):
return templates.TemplateResponse(request=request, name="export.html", context={"username": username})
@app.get("/api/export/json")
async def api_export_json(username: str = Depends(get_current_admin)):
"""Экспорт данных в JSON с прямой отдачей файла"""
import json
from database.db import AsyncSessionLocal
export_dir = Path(__file__).parent.parent / "exports"
export_dir.mkdir(exist_ok=True)
# Время по Ульяновску для имени файла
now_uly = datetime.now(ULY_TZ)
timestamp = now_uly.strftime("%Y%m%d_%H%M%S")
filename = f"domovoy_export_{timestamp}.json"
filepath = export_dir / filename
try:
@app.get("/schedules", response_class=HTMLResponse)
async def schedules_page(request: Request, username: str = Depends(get_current_admin)):
async with AsyncSessionLocal() as session:
# Получаем ВСЕХ пользователей
users_res = await session.execute(select(User))
users = users_res.scalars().all()
schedules = (await session.execute(select(Schedule).order_by(Schedule.start_time.desc()))).scalars().all()
return templates.TemplateResponse("schedules.html", {"request": request, "username": username, "schedules": schedules})
# Получаем ВСЕ сообщения (без лимита 1000)
messages_res = await session.execute(select(Message).order_by(Message.timestamp.asc()))
messages = messages_res.scalars().all()
# ============================================================================
# РАССЫЛКИ (SMART BROADCAST)
# ============================================================================
data = {
"export_info": {
"date": fmt_local(now_uly),
"total_users": len(list(users)),
"total_messages": len(list(messages))
},
"users": [
{
"user_id": u.user_id, "username": u.username, "first_name": u.first_name,
"last_name": u.last_name, "apartment": u.apartment, "phone": u.phone,
"verified": u.verified, "rating": u.rating, "level": u.level
} for u in users
],
"messages": [
{
"id": m.id, "user_id": m.user_id, "text": m.text,
"date": fmt_local(m.timestamp), "topic": m.topic
} for m in messages
]
}
@app.get("/broadcast", response_class=HTMLResponse)
async def broadcast_page(request: Request, username: str = Depends(get_current_admin)):
async with AsyncSessionLocal() as session:
broadcasts = (await session.execute(select(Broadcast).order_by(Broadcast.created_at.desc()).limit(50))).scalars().all()
broadcasts_data = []
for b in broadcasts:
read_count = (await session.execute(select(func.count(BroadcastRead.id)).where(BroadcastRead.broadcast_id == b.id))).scalar() or 0
broadcasts_data.append({
'id': b.id, 'text': b.text[:100] + '...' if len(b.text) > 100 else b.text,
'sent_at': fmt_local(b.sent_at) if b.sent_at else '-',
'total_sent': b.total_sent, 'read_count': read_count,
'read_percent': round((read_count / b.total_sent * 100), 1) if b.total_sent > 0 else 0,
'broadcast_type': b.broadcast_type or 'regular',
})
return templates.TemplateResponse("broadcast.html", {"request": request, "username": username, "broadcasts": broadcasts_data})
with open(filepath, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
# Отдаем файл напрямую в браузер
return FileResponse(
path=filepath,
filename=filename,
media_type='application/json'
)
except Exception as e:
logger.error(f"❌ Export error: {e}", exc_info=True)
return JSONResponse(status_code=500, content={"success": False, "error": str(e)})
# ============================================================================
# ЗАПЛАНИРОВАННЫЕ ПОСТЫ
# ============================================================================
@app.get("/scheduled_posts", response_class=HTMLResponse)
async def scheduled_posts_page(request: Request, username: str = Depends(get_current_admin)):
return templates.TemplateResponse(request=request, name="scheduled_posts.html", context={"username": username})
return templates.TemplateResponse("scheduled_posts.html", {"request": request, "username": username})
@app.get("/api/scheduled_posts/list")
async def api_list_scheduled_posts(username: str = Depends(get_current_admin)):
async with AsyncSessionLocal() as session:
posts = (await session.execute(select(ScheduledPost).order_by(ScheduledPost.scheduled_time.desc()))).scalars().all()
posts_data = []
for post in posts:
posts_data.append({
"id": post.id, "text": post.text, "topic_name": post.topic_name,
"topic_emoji": post.get_topic_emoji(), "recipients": post.recipients,
"scheduled_time": fmt_local(post.scheduled_time), "status": post.status,
"status_emoji": post.get_status_emoji(), "has_photo": bool(post.photo_file_id)
})
return JSONResponse({"success": True, "posts": posts_data})
# ============================================================================
# АУДИТ ПОЧТЫ
# ДАЙДЖЕСТЫ (V3.1)
# ============================================================================
@app.get("/digests", response_class=HTMLResponse)
async def digests_page(request: Request, username: str = Depends(get_current_admin)):
async with AsyncSessionLocal() as session:
# Получаем список дайджестов из БД
stmt_pending = select(Digest).where(Digest.status.in_(['draft', 'pending_approval'])).order_by(Digest.created_at.desc())
pending = (await session.execute(stmt_pending)).scalars().all()
stmt_archive = select(Digest).where(Digest.status.in_(['approved', 'sent', 'rejected'])).order_by(Digest.created_at.desc()).limit(20)
archive = (await session.execute(stmt_archive)).scalars().all()
return templates.TemplateResponse("digests.html", {
"request": request,
"username": username,
"pending_digests": pending,
"archive_digests": archive
})
@app.post("/api/digest/generate")
async def api_generate_digest(username: str = Depends(get_current_admin)):
from services.digest_service import DigestService
try:
stats = await DigestService.collect_weekly_stats()
digest_text = DigestService.format_digest_text(stats)
now = datetime.now()
week_num = now.isocalendar()[1]
async with AsyncSessionLocal() as session:
new_digest = Digest(
year=now.year,
week_number=week_num,
period_start=now - timedelta(days=7),
period_end=now,
digest_text=digest_text,
status='draft'
)
session.add(new_digest)
await session.commit()
return JSONResponse({"success": True, "message": f"Дайджест за {week_num} неделю успешно сгенерирован (черновик)"})
except Exception as e:
logger.error(f"Error generating digest: {e}", exc_info=True)
return JSONResponse(status_code=500, content={"success": False, "error": str(e)})
@app.get("/api/digest/{digest_id}")
async def api_get_digest(digest_id: int, username: str = Depends(get_current_admin)):
async with AsyncSessionLocal() as session:
digest = await session.get(Digest, digest_id)
if not digest: return JSONResponse({"error": "Not found"}, status_code=404)
return JSONResponse({
"digest": {
"id": digest.id,
"week": digest.week_number,
"year": digest.year,
"text": digest.digest_text,
"status": digest.status
}
})
# ============================================================================
# ЭКСПОРТ (JSON + GITEA)
# ============================================================================
@app.get("/export", response_class=HTMLResponse)
async def export_page(request: Request, username: str = Depends(get_current_admin)):
from services.chat_exporter import ChatExporter
async with AsyncSessionLocal() as session:
exporter = ChatExporter(session)
stats = await exporter.get_stats()
return templates.TemplateResponse("export.html", {"request": request, "username": username, "export_stats": stats, "fmt_local": fmt_local})
@app.get("/api/export/json")
async def api_export_json(username: str = Depends(get_current_admin)):
from services.chat_exporter import ChatExporter
try:
async with AsyncSessionLocal() as session:
exporter = ChatExporter(session)
result = await exporter.run_full_export()
if not result.get('success'): return JSONResponse(status_code=500, content=result)
return JSONResponse(result)
except Exception as e:
return JSONResponse(status_code=500, content={"success": False, "error": str(e)})
@app.get("/exports/{filename}")
async def download_export(filename: str, username: str = Depends(get_current_admin)):
filepath = BASE_DIR / "exports" / filename
if not filepath.exists(): raise HTTPException(status_code=404, detail="File not found")
return FileResponse(filepath, filename=filename, media_type='application/json')
# ============================================================================
# ПОЧТА (EMAIL AUDITOR)
# ============================================================================
@app.get("/emails", response_class=HTMLResponse)
async def emails_page(request: Request, username: str = Depends(get_current_admin)):
async with AsyncSessionLocal() as session:
# Получаем статистику по доменам
stmt = select(EmailAudit).order_by(EmailAudit.last_email_date.desc())
audit_data = list((await session.execute(stmt)).scalars().all())
audit_data = (await session.execute(select(EmailAudit).order_by(EmailAudit.last_email_date.desc()))).scalars().all()
last_log = (await session.execute(select(EmailExportLog).order_by(EmailExportLog.start_time.desc()).limit(1))).scalar_one_or_none()
# Получаем логи последнего экспорта
stmt_log = select(EmailExportLog).order_by(EmailExportLog.start_time.desc()).limit(1)
last_log = (await session.execute(stmt_log)).scalar_one_or_none()
# Получаем список последних 10 файлов (писем)
latest_emails = []
all_folder = base_dir / "services" / "email_auditor" / "exported_emails" / "all"
all_folder = BASE_DIR / "services" / "email_auditor" / "exported_emails" / "all"
if all_folder.exists():
files = sorted(all_folder.glob("*.pdf"), key=lambda x: x.stat().st_mtime, reverse=True)[:10]
for f in files:
name = f.stem
# Парсим: YYYY-MM-DD_domain_subject
match = re.match(r'(\d{4}-\d{2}-\d{2})_([^_]+)_(.*)', name)
if match:
date_str, domain, subject = match.groups()
latest_emails.append({"date": date_str, "domain": domain, "subject": subject})
else:
# Вариант 2: domain_date_sender_subject
match = re.match(r'([^_]+)_(\d{4}-\d{2}-\d{2})_(.*)', name)
if match:
domain, date_str, rest = match.groups()
latest_emails.append({"date": date_str, "domain": domain, "subject": rest})
match = re.match(r'(\d{4}-\d{2}-\d{2})_([^_]+)_(.*)', f.stem)
if match: latest_emails.append({"date": match.group(1), "domain": match.group(2), "subject": match.group(3)})
# Проверяем наличие мастер-файла
master_pdf_path = Path(__file__).parent.parent / "services" / "email_auditor" / "exported_emails" / "TOTAL_ARCHIVE_2025-2026.pdf"
has_master_pdf = master_pdf_path.exists()
last_update = None
if has_master_pdf:
mtime = master_pdf_path.stat().st_mtime
# Берем время как UTC
last_update = datetime.fromtimestamp(mtime, tz=timezone.utc)
master_pdf_path = BASE_DIR / "services" / "email_auditor" / "exported_emails" / "TOTAL_ARCHIVE_2025-2026.pdf"
last_update = datetime.fromtimestamp(master_pdf_path.stat().st_mtime, tz=timezone.utc) if master_pdf_path.exists() else None
return templates.TemplateResponse(
request=request, name="emails.html",
context={
"username": username, "audit_data": audit_data, "last_log": last_log,
"latest_emails": latest_emails,
"has_master_pdf": has_master_pdf, "last_update": last_update,
"fmt_local": fmt_local
}
)
return templates.TemplateResponse("emails.html", {
"request": request, "username": username, "audit_data": audit_data, "last_log": last_log,
"latest_emails": latest_emails, "has_master_pdf": master_pdf_path.exists(), "last_update": last_update, "fmt_local": fmt_local
})
@app.post("/api/emails/run")
async def api_run_email_audit(username: str = Depends(get_current_admin)):
"""Запуск аудита почты вручную (фоновым процессом)"""
import subprocess
import sys
import os
# Определяем пути
base_dir = Path(__file__).parent.parent
script_path = base_dir / "services" / "email_auditor" / "export_emails_v2.py"
cwd_path = base_dir / "services" / "email_auditor"
# Ищем питон в venv
venv_python = base_dir / "venv" / "bin" / "python3"
if not venv_python.exists():
# Попробуем просто python3 если venv не найден
venv_python = Path(sys.executable)
if not script_path.exists():
return JSONResponse(status_code=404, content={"message": f"Скрипт не найден: {script_path}"})
logger.info(f"🚀 Manual audit attempt. Python: {venv_python}, Script: {script_path}")
script_path = BASE_DIR / "services" / "email_auditor" / "export_emails_v2.py"
venv_python = BASE_DIR / "venv" / "bin" / "python3"
if not script_path.exists(): return JSONResponse(status_code=404, content={"message": "Script not found"})
try:
# Устанавливаем PYTHONPATH
env = os.environ.copy()
env["PYTHONPATH"] = str(base_dir)
# Открываем лог-файл для записи вывода скрипта
audit_log_path = base_dir / "logs" / "manual_audit.log"
audit_log = open(audit_log_path, "a")
# Запускаем скрипт в фоне
process = subprocess.Popen(
[str(venv_python), str(script_path)],
cwd=str(cwd_path),
env=env,
stdout=audit_log,
stderr=audit_log,
start_new_session=True
)
return JSONResponse(content={"message": f"Процесс аудита успешно запущен (PID: {process.pid}). Результат придет в Telegram."})
env["PYTHONPATH"] = str(BASE_DIR)
process = subprocess.Popen([str(venv_python), str(script_path)], cwd=str(BASE_DIR / "services" / "email_auditor"), env=env, start_new_session=True)
return JSONResponse(content={"message": f"Audit started (PID: {process.pid})"})
except Exception as e:
logger.error(f"❌ Subprocess error: {e}", exc_info=True)
return JSONResponse(status_code=500, content={"message": f"Ошибка запуска процесса: {str(e)}"})
def run_web_server(host: str = "0.0.0.0", port: int = 8000):
import uvicorn
uvicorn.run(app, host=host, port=port)
return JSONResponse(status_code=500, content={"message": str(e)})
if __name__ == "__main__":
run_web_server()
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

1547
web/app_reference.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -68,11 +68,11 @@
{% if pending_digests|length > 0 %}
{% for digest in pending_digests %}
<div class="pending-card">
<h3>📰 Дайджест #{{ digest.week }}/{{ digest.year }}</h3>
<h3>📰 Дайджест #{{ digest.week_number }}/{{ digest.year }}</h3>
<div class="meta">
Период: {{ digest.period_label }} | Создан: {{ digest.created_at }} | Статус: <span class="badge badge-pending">{{ digest.status_emoji }} {{ digest.status }}</span>
</div>
<div class="text">{{ digest.text }}</div>
<div class="text">{{ digest.digest_text }}</div>
<div style="margin-top: 15px;">
<button class="btn btn-approve" onclick="approveDigest({{ digest.id }})">✅ Утвердить</button>
<button class="btn btn-edit" onclick="editDigest({{ digest.id }})">✏️ Редактировать</button>
@ -105,7 +105,7 @@
<tbody>
{% for digest in archive_digests %}
<tr>
<td>#{{ digest.week }}/{{ digest.year }}</td>
<td>#{{ digest.week_number }}/{{ digest.year }}</td>
<td>{{ digest.period_label }}</td>
<td><span class="badge badge-{{ digest.status }}">{{ digest.status_emoji }} {{ digest.status }}</span></td>
<td>{{ digest.sent_at }}</td>

View file

@ -10,12 +10,12 @@
<div class="col-md-6">
<div class="card border-success">
<div class="card-header border-success">
<i class="bi bi-file-earmark-json"></i> ЭКСПОРТ В JSON
<i class="bi bi-file-earmark-json"></i> ЭКСПОРТ В JSON & GITEA
</div>
<div class="card-body">
<p>Экспорт всех пользователей и сообщений в JSON формат.</p>
<p>Экспорт ВСЕХ пользователей и сообщений. Файл будет автоматически отправлен в Gitea.</p>
<button class="btn btn-matrix w-100" onclick="exportJSON()">
<i class="bi bi-download"></i> ЭКСПОРТИРОВАТЬ
<i class="bi bi-cloud-arrow-up"></i> ЗАПУСТИТЬ ПОЛНЫЙ ЭКСПОРТ
</button>
<div id="exportResult" class="mt-3"></div>
</div>
@ -25,18 +25,17 @@
<div class="col-md-6">
<div class="card border-success">
<div class="card-header border-success">
<i class="bi bi-info-circle"></i> ИНФОРМАЦИЯ
<i class="bi bi-graph-up"></i> СТАТИСТИКА ВЫГРУЗОК
</div>
<div class="card-body">
<p>Экспорт включает:</p>
<ul class="text-success opacity-75">
<li>Всех пользователей</li>
<li>Все сообщения</li>
<li>Даты и время</li>
<li>Квартиры и телефоны</li>
</ul>
<div class="h4 text-matrix mb-3">
Всего выгрузок: <span class="badge bg-success">{{ export_stats.total }}</span>
</div>
<p>Последняя выгрузка:<br>
<span class="text-success">{{ fmt_local(export_stats.last_date) if export_stats.last_date else 'Никогда' }}</span></p>
<hr class="border-success">
<p class="text-muted small">
Файл сохраняется в директории <code>exports/</code>
Автоматический экспорт выполняется раз в неделю (вс, 03:00) и пушится в репозиторий.
</p>
</div>
</div>
@ -48,44 +47,37 @@
<script>
async function exportJSON() {
const resultDiv = document.getElementById('exportResult');
resultDiv.innerHTML = '<div class="alert alert-info"><div class="spinner-border spinner-border-sm me-2"></div>Генерация файла...</div>';
const btn = document.querySelector('button[onclick="exportJSON()"]');
resultDiv.innerHTML = '<div class="alert alert-info"><div class="spinner-border spinner-border-sm me-2"></div>Сбор данных и отправка в Gitea...</div>';
btn.disabled = true;
try {
const response = await fetch('/api/export/json');
const data = await response.json();
if (response.ok) {
// Получаем файл как blob
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
// Извлекаем имя файла из заголовка или генерим сами
const contentDisposition = response.headers.get('Content-Disposition');
let fileName = 'domovoy_export.json';
if (contentDisposition && contentDisposition.indexOf('filename=') !== -1) {
fileName = contentDisposition.split('filename=')[1].replace(/"/g, '');
}
// Создаем невидимую ссылку для скачивания
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
a.remove();
if (response.ok && data.success) {
resultDiv.innerHTML = `
<div class="alert alert-success">
✅ Файл успешно сгенерирован и отправлен в загрузки!<br>
<small>Имя файла: ${fileName}</small>
<b>Успех!</b><br>
Файл: <code>${data.filename}</code><br>
Сообщений: ${data.messages}<br>
Статус Git: ${data.git ? '🚀 Отправлено в Gitea' : '❌ Ошибка Git'}<br>
<br>
<a href="/exports/${data.filename}" class="btn btn-outline-success btn-sm w-100 mb-2">
<i class="bi bi-download"></i> СКАЧАТЬ ФАЙЛ ЛОКАЛЬНО
</a>
<small>Страница обновится через 10 сек...</small>
</div>
`;
setTimeout(() => location.reload(), 10000);
} else {
const errorData = await response.json();
resultDiv.innerHTML = `<div class="alert alert-danger">❌ Ошибка: ${errorData.error || 'Неизвестная ошибка'}</div>`;
resultDiv.innerHTML = `<div class="alert alert-danger">❌ Ошибка: ${data.error || 'Неизвестно'}</div>`;
}
} catch (error) {
resultDiv.innerHTML = `<div class="alert alert-danger">❌ Ошибка сети: ${error.message}</div>`;
} finally {
btn.disabled = false;
}
}
</script>