diff --git a/.gitignore b/.gitignore
index 00bed06..5b1240f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,7 +31,6 @@ logs/
*.log
# Exports
-exports/
backups/
# OS
diff --git a/EXPORTS_FIX_LOG.md b/EXPORTS_FIX_LOG.md
new file mode 100644
index 0000000..196229e
--- /dev/null
+++ b/EXPORTS_FIX_LOG.md
@@ -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 прокси не нужен. Скрипт сам сбрасывает переменные окружения при пуше.
diff --git a/database/migrate_exports.py b/database/migrate_exports.py
new file mode 100644
index 0000000..bf513be
--- /dev/null
+++ b/database/migrate_exports.py
@@ -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 создана")
diff --git a/exports/chat_export_20260228_124720.json b/exports/chat_export_20260228_124720.json
new file mode 100644
index 0000000..19811bf
--- /dev/null
+++ b/exports/chat_export_20260228_124720.json
@@ -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": []
+}
\ No newline at end of file
diff --git a/exports/chat_export_20260302_030000.json b/exports/chat_export_20260302_030000.json
new file mode 100644
index 0000000..3feab42
--- /dev/null
+++ b/exports/chat_export_20260302_030000.json
@@ -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": []
+}
\ No newline at end of file
diff --git a/exports/chat_export_20260306_192022.json b/exports/chat_export_20260306_192022.json
new file mode 100644
index 0000000..9a23f14
--- /dev/null
+++ b/exports/chat_export_20260306_192022.json
@@ -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": []
+}
\ No newline at end of file
diff --git a/exports/chat_export_20260306_192032.json b/exports/chat_export_20260306_192032.json
new file mode 100644
index 0000000..428db76
--- /dev/null
+++ b/exports/chat_export_20260306_192032.json
@@ -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": []
+}
\ No newline at end of file
diff --git a/exports/chat_export_20260309_030000.json b/exports/chat_export_20260309_030000.json
new file mode 100644
index 0000000..0ca6d00
--- /dev/null
+++ b/exports/chat_export_20260309_030000.json
@@ -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": []
+}
\ No newline at end of file
diff --git a/exports/chat_export_20260311_184038.json b/exports/chat_export_20260311_184038.json
new file mode 100644
index 0000000..5a1cab1
--- /dev/null
+++ b/exports/chat_export_20260311_184038.json
@@ -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
+ }
+ ]
+}
\ No newline at end of file
diff --git a/exports/chat_export_20260316_182048.json b/exports/chat_export_20260316_182048.json
new file mode 100644
index 0000000..36767ac
--- /dev/null
+++ b/exports/chat_export_20260316_182048.json
@@ -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
+ }
+ ]
+}
\ No newline at end of file
diff --git a/exports/chat_export_20260322_230000.json b/exports/chat_export_20260322_230000.json
new file mode 100644
index 0000000..ab6a08f
--- /dev/null
+++ b/exports/chat_export_20260322_230000.json
@@ -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
+ }
+ ]
+}
\ No newline at end of file
diff --git a/exports/chat_export_20260329_230000.json b/exports/chat_export_20260329_230000.json
new file mode 100644
index 0000000..685c633
--- /dev/null
+++ b/exports/chat_export_20260329_230000.json
@@ -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
+ }
+ ]
+}
\ No newline at end of file
diff --git a/exports/chat_export_20260405_230000.json b/exports/chat_export_20260405_230000.json
new file mode 100644
index 0000000..47dae1b
--- /dev/null
+++ b/exports/chat_export_20260405_230000.json
@@ -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
+ }
+ ]
+}
\ No newline at end of file
diff --git a/exports/chat_export_20260412_230000.json b/exports/chat_export_20260412_230000.json
new file mode 100644
index 0000000..6a63a23
--- /dev/null
+++ b/exports/chat_export_20260412_230000.json
@@ -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
+ }
+ ]
+}
\ No newline at end of file
diff --git a/exports/chat_history_2026-04-18_22-31.json b/exports/chat_history_2026-04-18_22-31.json
new file mode 100644
index 0000000..2a45bc1
--- /dev/null
+++ b/exports/chat_history_2026-04-18_22-31.json
@@ -0,0 +1,1474 @@
+{
+ "info": {
+ "exported_at": "2026-04-18T22:31:13.427579+04:00",
+ "total_users": 39,
+ "total_messages": 170
+ },
+ "users": [
+ {
+ "id": 197957361,
+ "username": "ktybdsq",
+ "name": "Александр MatrixHasYou",
+ "apt": "152",
+ "verified": true
+ },
+ {
+ "id": 5475262081,
+ "username": null,
+ "name": "Надежда Тырина",
+ "apt": "152",
+ "verified": true
+ },
+ {
+ "id": 8725618164,
+ "username": "lkmthreeseven_bot",
+ "name": "DomovoyKontrolBot",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 8181748817,
+ "username": null,
+ "name": "Фаина",
+ "apt": "186",
+ "verified": true
+ },
+ {
+ "id": 1867403697,
+ "username": "cvetok_nura",
+ "name": "Светлана",
+ "apt": "171",
+ "verified": true
+ },
+ {
+ "id": 1335945859,
+ "username": null,
+ "name": "O4aPoBaHHblu_cTpaHHuK",
+ "apt": "",
+ "verified": true
+ },
+ {
+ "id": 1299551170,
+ "username": "katt_rin9",
+ "name": "Екатерина🐈",
+ "apt": "205",
+ "verified": true
+ },
+ {
+ "id": 1970561536,
+ "username": null,
+ "name": "Окся",
+ "apt": "255 КВ",
+ "verified": true
+ },
+ {
+ "id": 576801733,
+ "username": "VolkovaElena73",
+ "name": "Елена Волкова Ульяновск",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 1136316705,
+ "username": "IrinaTs15",
+ "name": "Irina Tsyganova",
+ "apt": "319",
+ "verified": true
+ },
+ {
+ "id": 462572897,
+ "username": null,
+ "name": "Алекс",
+ "apt": "48",
+ "verified": true
+ },
+ {
+ "id": 259925158,
+ "username": "Izuma_net",
+ "name": "izuma_net",
+ "apt": "295",
+ "verified": true
+ },
+ {
+ "id": 861072224,
+ "username": "AFadeeva",
+ "name": "Александра🌤",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 1670557872,
+ "username": "KhaykinaAnastasia",
+ "name": "Анастасия",
+ "apt": "295",
+ "verified": true
+ },
+ {
+ "id": 1179832004,
+ "username": "st5ksu",
+ "name": "Kseniya Stepanova",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 1432214116,
+ "username": "pro100beze",
+ "name": "Алёна",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 1861008160,
+ "username": null,
+ "name": "Елена",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 2139751123,
+ "username": null,
+ "name": "Евгений Петрович",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 5246728667,
+ "username": "valeriya_nagini",
+ "name": "Валерия",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 7193750921,
+ "username": null,
+ "name": "Светлана",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 7339698511,
+ "username": null,
+ "name": "Елена",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 368569899,
+ "username": "Countesss",
+ "name": "Анастасия Лютерова",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 6344554911,
+ "username": null,
+ "name": "Наташа 🌹",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 5166131650,
+ "username": null,
+ "name": "Николай Петрович",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 1667052403,
+ "username": null,
+ "name": "Ольга",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 5167606723,
+ "username": null,
+ "name": "Tatiyana",
+ "apt": "297",
+ "verified": true
+ },
+ {
+ "id": 5183810334,
+ "username": null,
+ "name": "Леонид",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 2044370304,
+ "username": "Brasletvz73",
+ "name": "Браслет",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 6654418281,
+ "username": "snez_frolova99",
+ "name": "Твоя ногтевая фея💅🏼",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 864147545,
+ "username": "roman_garipov",
+ "name": "Roman",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 5036016769,
+ "username": null,
+ "name": "Алсу Юсупова",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 1476737008,
+ "username": null,
+ "name": "Елена",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 772234481,
+ "username": null,
+ "name": "Анастасия",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 5235580033,
+ "username": null,
+ "name": "Александр",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 458413170,
+ "username": "MeLnikov_Alesha",
+ "name": "Алексей Мельников",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 7346013939,
+ "username": null,
+ "name": "Любовь Землянская",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 1353946650,
+ "username": null,
+ "name": "ГеннадийП",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 5106504485,
+ "username": null,
+ "name": "Ирина",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 7702466390,
+ "username": "Ulik8319",
+ "name": "Юлия",
+ "apt": "287",
+ "verified": true
+ }
+ ],
+ "messages": [
+ {
+ "id": 1,
+ "user_id": 8181748817,
+ "text": "Это отчет УО за 2025г.\nТатьяна Мих. расклеила",
+ "date": "2026-03-11T17:06:13.912964",
+ "topic": null
+ },
+ {
+ "id": 2,
+ "user_id": 197957361,
+ "text": "Такая куча ссылок на какие то акты - да с номерами !\nФаина явно заинтересует вам что то из актов !",
+ "date": "2026-03-11T17:16:35.705412",
+ "topic": null
+ },
+ {
+ "id": 3,
+ "user_id": 197957361,
+ "text": "162 квартира, 5й подьезд - топят всех \n\nкто нибудь знает кто живет там в 2шке ?",
+ "date": "2026-03-11T18:27:49.681833",
+ "topic": null
+ },
+ {
+ "id": 4,
+ "user_id": 197957361,
+ "text": "аварийку вызвали, перекрыли воду и уехали \nдверь никто не открывает, никого нету",
+ "date": "2026-03-11T18:49:43.652398",
+ "topic": "вода"
+ },
+ {
+ "id": 5,
+ "user_id": 197957361,
+ "text": "❔Кто нибудь в нашем доме понимает - каким таким образом ЖСС считает, что бумажка без подписи и печати на подьезде позволяет им навязать нам, собственникам, какой то выдуманный тариф с марта ? \n\nУже сколько они тарифов предлагали нам ? со счета сбился я😆\nВсе разные\nНи один не обоснован ничем\n\nИ вот - теперь вообще спустя рукава, бумажку из принтера повесили - вот вам новый тариф! \n\nЧто то нету таких вариантов в Жилищном Кодексе РФ, где четко написано, что этот вопрос решается только на Общем Собрании Собственников путем голосования\n\nАх да! на бумажке же ссылка на какой то договор управления домом от 2015 года... мммм. \nА кто нибудь его открывал ? \nДоговор №65? он в ГИС ЖКХ висит у всех\n\n✅Договор № 65 от 01.12.2015 года подписан со стороны «собственника» председателем Комитета ЖКХ администрации г. Ульяновска Ворожецовым А.В.\n\nСогласно ч. 1 ст. 162 ЖК РФ, договор управления заключается с каждым собственником в письменной форме\n. Чиновник Ворожецов не имел права подписывать условия (включая автоматическую индексацию) за частных собственников приватизированных квартир\n\nСсылка ЖСС на этот договор как на основание для индексации в 2026 году — это попытка распространить условия сделки, совершенной третьими лицами (администрацией), на жителей дома без их согласия.\n\n\n✅Факты: В Протоколе № Лен.Ком.37/1 от 29.01.2025 года собственники уже реализовали свое право по ст. 156 ЖК РФ и утвердили конкретный тариф — 37,50 руб./м² на период до 31.01.2026 года\n.\nНарушение: ЖСС в своем уведомлении о росте тарифа до 40,80 руб.\n полностью игнорирует тот факт, что механизм индексации из «старого» договора (п. 3.6)\n был «убит» принятием конкретного решения собственников в 2025 году. Согласно ч. 7 ст. 156 ЖК РФ, размер платы устанавливается только на общем собрании\n. Раз жители в 2025 году установили твердую цифру, никакая «автоматическая индексация» больше не работает.",
+ "date": "2026-03-12T17:02:51.184428",
+ "topic": "собрание"
+ },
+ {
+ "id": 6,
+ "user_id": 197957361,
+ "text": "Вот смеху то будет, если в апреле такая платежка прийдет ! 😃\n\nКак только придет первая платежка с тарифом выше 37,50 руб., подаем массовые требования о выплате штрафа 50% от суммы превышения на основании ч. 11 ст. 156 ЖК РФ\n\nНу, кто хочет платить по выдуманному тарифу может конечно при своем остаться, у нас же свобода воли полная (но не по мнению ЖСС 😁)",
+ "date": "2026-03-12T17:04:16.699206",
+ "topic": "уборка"
+ },
+ {
+ "id": 7,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-03-17T11:44:28.416187",
+ "topic": null
+ },
+ {
+ "id": 8,
+ "user_id": 8181748817,
+ "text": "Смешной штраф🙈",
+ "date": "2026-03-17T11:44:28.638147",
+ "topic": null
+ },
+ {
+ "id": 9,
+ "user_id": 576801733,
+ "text": "Спасибки, завтра доеду)))",
+ "date": "2026-03-17T11:44:28.723239",
+ "topic": "благодарности"
+ },
+ {
+ "id": 10,
+ "user_id": 1179832004,
+ "text": "Значит вам нужно на Киевский,они установят вам приложение",
+ "date": "2026-03-17T11:44:28.742319",
+ "topic": null
+ },
+ {
+ "id": 11,
+ "user_id": 1432214116,
+ "text": null,
+ "date": "2026-03-17T11:44:28.763231",
+ "topic": null
+ },
+ {
+ "id": 12,
+ "user_id": 576801733,
+ "text": "Он выдаёт мне на андроид опять vdome, а оно не пашет((((",
+ "date": "2026-03-17T11:44:28.863145",
+ "topic": null
+ },
+ {
+ "id": 13,
+ "user_id": 1670557872,
+ "text": "Понятно, спасибо",
+ "date": "2026-03-17T11:44:28.903963",
+ "topic": "благодарности"
+ },
+ {
+ "id": 14,
+ "user_id": 1861008160,
+ "text": null,
+ "date": "2026-03-17T11:44:28.923567",
+ "topic": null
+ },
+ {
+ "id": 15,
+ "user_id": 5246728667,
+ "text": "Забрали уже.",
+ "date": "2026-03-17T11:44:28.952646",
+ "topic": null
+ },
+ {
+ "id": 16,
+ "user_id": 2139751123,
+ "text": "Отписка пять баллов 👍",
+ "date": "2026-03-17T11:44:28.974797",
+ "topic": null
+ },
+ {
+ "id": 17,
+ "user_id": 1299551170,
+ "text": "Через госуслуги попробуйте связаться",
+ "date": "2026-03-17T11:44:29.175932",
+ "topic": null
+ },
+ {
+ "id": 18,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-03-17T17:59:33.059721",
+ "topic": null
+ },
+ {
+ "id": 19,
+ "user_id": 7339698511,
+ "text": "Люди!!! Дайте ссылки на домофон и на камеры???",
+ "date": "2026-03-19T05:28:21.047935",
+ "topic": "домофон"
+ },
+ {
+ "id": 20,
+ "user_id": 7339698511,
+ "text": "Или напишите адреса куда можно обратиться по поводу камер и домофона, пожалуйста",
+ "date": "2026-03-19T05:29:12.037940",
+ "topic": "вода"
+ },
+ {
+ "id": 21,
+ "user_id": 368569899,
+ "text": "Елена, я вам написала в личные сообщения",
+ "date": "2026-03-19T06:07:28.399172",
+ "topic": null
+ },
+ {
+ "id": 22,
+ "user_id": 7339698511,
+ "text": "Спасибо большое",
+ "date": "2026-03-19T06:17:47.232121",
+ "topic": "благодарности"
+ },
+ {
+ "id": 23,
+ "user_id": 6344554911,
+ "text": "Здравствуйте. Кто знает телефон нашего сантехника, дайте пожалуйста",
+ "date": "2026-03-19T12:26:07.174734",
+ "topic": "вопросы"
+ },
+ {
+ "id": 24,
+ "user_id": 259925158,
+ "text": "Попробуйте этот 8 (996) 219-95-67",
+ "date": "2026-03-19T12:45:37.451726",
+ "topic": null
+ },
+ {
+ "id": 25,
+ "user_id": 6344554911,
+ "text": "Спасибо",
+ "date": "2026-03-19T12:48:10.923728",
+ "topic": "благодарности"
+ },
+ {
+ "id": 26,
+ "user_id": 197957361,
+ "text": "А как его зовут ? Может его в бота в раздел Мастера записать ?",
+ "date": "2026-03-19T15:28:58.258357",
+ "topic": null
+ },
+ {
+ "id": 27,
+ "user_id": 6344554911,
+ "text": "Его Алексей зовут",
+ "date": "2026-03-19T15:32:23.718845",
+ "topic": null
+ },
+ {
+ "id": 28,
+ "user_id": 5166131650,
+ "text": "Напротив 5 подъезда?",
+ "date": "2026-03-20T11:35:38.550863",
+ "topic": "уборка"
+ },
+ {
+ "id": 29,
+ "user_id": 5166131650,
+ "text": "Напротив 6 подъезда?",
+ "date": "2026-03-20T11:37:13.711268",
+ "topic": "уборка"
+ },
+ {
+ "id": 30,
+ "user_id": 1667052403,
+ "text": "Вы о чём?",
+ "date": "2026-03-20T11:47:26.526343",
+ "topic": null
+ },
+ {
+ "id": 31,
+ "user_id": 8181748817,
+ "text": null,
+ "date": "2026-03-20T14:14:46.056946",
+ "topic": null
+ },
+ {
+ "id": 32,
+ "user_id": 8181748817,
+ "text": null,
+ "date": "2026-03-20T14:14:46.089582",
+ "topic": null
+ },
+ {
+ "id": 33,
+ "user_id": 8181748817,
+ "text": null,
+ "date": "2026-03-20T14:14:46.113065",
+ "topic": null
+ },
+ {
+ "id": 34,
+ "user_id": 8181748817,
+ "text": null,
+ "date": "2026-03-20T14:14:46.136436",
+ "topic": null
+ },
+ {
+ "id": 35,
+ "user_id": 8181748817,
+ "text": null,
+ "date": "2026-03-20T14:14:46.161576",
+ "topic": null
+ },
+ {
+ "id": 36,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-03-21T07:45:12.249018",
+ "topic": null
+ },
+ {
+ "id": 37,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-03-21T07:47:26.209479",
+ "topic": null
+ },
+ {
+ "id": 38,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-03-21T09:36:37.219173",
+ "topic": null
+ },
+ {
+ "id": 39,
+ "user_id": 8181748817,
+ "text": null,
+ "date": "2026-03-21T10:40:53.080101",
+ "topic": null
+ },
+ {
+ "id": 40,
+ "user_id": 8181748817,
+ "text": null,
+ "date": "2026-03-21T10:40:53.117680",
+ "topic": null
+ },
+ {
+ "id": 41,
+ "user_id": 8181748817,
+ "text": null,
+ "date": "2026-03-21T10:40:53.162184",
+ "topic": null
+ },
+ {
+ "id": 42,
+ "user_id": 8181748817,
+ "text": "Вот договор, который нам дали на руки в УО ЖСС 27 февраля. В ГИС ЖКХ его нет.\nПо п.4.1.3. сказано, что запись из архива предоставляется по письменным запросам собственников МКД непосредственно в ООО \"ВидеоСервис\".\nПо п.2.2. записано, что Акты выполнкнеых работ подписывает Исполнитель, Заквзчик и ПСДома, на основании которых ежемесячно производится оплата.",
+ "date": "2026-03-21T10:50:22.689484",
+ "topic": "ук"
+ },
+ {
+ "id": 43,
+ "user_id": 8181748817,
+ "text": "Жители дома при инцидентах обращались к Т.Мих, так как договора никто ранее и не видел, кроме ПСдома.",
+ "date": "2026-03-21T10:54:32.497882",
+ "topic": null
+ },
+ {
+ "id": 44,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-03-21T10:57:54.609831",
+ "topic": null
+ },
+ {
+ "id": 45,
+ "user_id": 8181748817,
+ "text": "Если с января 26г услуга не оказывается, почему Акты ПСДома подписывает. \nЯ лично за янв- февр.за камеры не плачу.",
+ "date": "2026-03-21T10:58:35.386487",
+ "topic": null
+ },
+ {
+ "id": 46,
+ "user_id": 197957361,
+ "text": "Какой же нормальный человек будет в магазине покупая диван, заключать договор не между собой и магазином - а подписывать какой то договор между Магазином и другим человеком ? \nОн потом как в суд пойдет если что случится ? \nВ суде пальцем у виска покрутят и скажут \"Это ж не ваш договор, чего вы на него ссылаетесь ? \"",
+ "date": "2026-03-21T10:58:50.056137",
+ "topic": "домофон"
+ },
+ {
+ "id": 47,
+ "user_id": 197957361,
+ "text": "да ПСД дома подпишет все, что ей ЖСС даст, не глядя и забыв про бухгалтерию 😀😂",
+ "date": "2026-03-21T10:59:18.563614",
+ "topic": null
+ },
+ {
+ "id": 48,
+ "user_id": 8181748817,
+ "text": "Там приложено Доп.согл.11 от 01.02.24г, которым наш дом присоединился",
+ "date": "2026-03-21T11:00:57.340571",
+ "topic": null
+ },
+ {
+ "id": 49,
+ "user_id": 462572897,
+ "text": "Фаина, а как понять, что с января 26 г. услуга не оказывается?",
+ "date": "2026-03-21T11:50:50.112523",
+ "topic": null
+ },
+ {
+ "id": 50,
+ "user_id": 8181748817,
+ "text": "Я пишу: \"если с января...\" так как в чате жители писали, что не могут получить доступ к видео.\nИ в январе исполнитель с Видеосервис говорил, что с января 26г.доступ к архиву с разрешения Погодина.",
+ "date": "2026-03-21T12:04:13.443501",
+ "topic": "уборка"
+ },
+ {
+ "id": 51,
+ "user_id": 8181748817,
+ "text": "Уважаемые жители дома!\nКто в янв- февр- марте обращался за доступом к видеоархиву и получил его? А кто не получил?",
+ "date": "2026-03-21T12:07:50.673653",
+ "topic": "уборка"
+ },
+ {
+ "id": 52,
+ "user_id": 197957361,
+ "text": "А за уборкой подьезда и заменой лампочек в подьезде - видимо тоже куда то надо обращаться ? \n\nУ меня ощущение у одного, что ЖСС как бы и не при чем ? \n\nНу старается показать что не при чем 😅",
+ "date": "2026-03-21T12:10:14.825495",
+ "topic": "уборка"
+ },
+ {
+ "id": 53,
+ "user_id": 1179832004,
+ "text": null,
+ "date": "2026-03-21T19:25:04.858395",
+ "topic": null
+ },
+ {
+ "id": 54,
+ "user_id": 5183810334,
+ "text": "Вы что уличный сторож?",
+ "date": "2026-03-21T21:27:31.050046",
+ "topic": null
+ },
+ {
+ "id": 55,
+ "user_id": 1179832004,
+ "text": "А вы наверное юморист?",
+ "date": "2026-03-22T04:46:48.764565",
+ "topic": null
+ },
+ {
+ "id": 56,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-03-22T11:38:52.025450",
+ "topic": null
+ },
+ {
+ "id": 57,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-03-22T11:38:52.034326",
+ "topic": null
+ },
+ {
+ "id": 58,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-03-23T10:04:38.017104",
+ "topic": null
+ },
+ {
+ "id": 59,
+ "user_id": 2044370304,
+ "text": null,
+ "date": "2026-03-24T14:50:38.299476",
+ "topic": null
+ },
+ {
+ "id": 60,
+ "user_id": 2044370304,
+ "text": "Товарищи, кто знает хозяина этой машины ?",
+ "date": "2026-03-24T14:53:36.062481",
+ "topic": "вопросы"
+ },
+ {
+ "id": 61,
+ "user_id": 2044370304,
+ "text": null,
+ "date": "2026-03-24T14:53:40.014838",
+ "topic": null
+ },
+ {
+ "id": 62,
+ "user_id": 6654418281,
+ "text": "Эвакуатор может вызвать ?",
+ "date": "2026-03-24T15:02:20.242206",
+ "topic": null
+ },
+ {
+ "id": 63,
+ "user_id": 1667052403,
+ "text": "Пишите в чат 5 дома. Может там хозяин",
+ "date": "2026-03-24T15:05:27.186789",
+ "topic": null
+ },
+ {
+ "id": 64,
+ "user_id": 2044370304,
+ "text": "Как в их чат написать ?",
+ "date": "2026-03-24T15:15:45.859199",
+ "topic": null
+ },
+ {
+ "id": 65,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-03-25T08:25:50.145257",
+ "topic": null
+ },
+ {
+ "id": 66,
+ "user_id": 1670557872,
+ "text": "Добрый день! Подскажите пожалуйста, кто-нибудь знает номер телефона организации по поверке счетчиков",
+ "date": "2026-03-25T09:22:49.504050",
+ "topic": "вопросы"
+ },
+ {
+ "id": 67,
+ "user_id": 1867403697,
+ "text": "+7 927 270-21-70 добрый день",
+ "date": "2026-03-25T09:23:59.518450",
+ "topic": null
+ },
+ {
+ "id": 68,
+ "user_id": 1867403697,
+ "text": "Мне вот они понравились )",
+ "date": "2026-03-25T09:24:06.325785",
+ "topic": null
+ },
+ {
+ "id": 69,
+ "user_id": 1670557872,
+ "text": "Большое спасибо!",
+ "date": "2026-03-25T09:24:23.524322",
+ "topic": "благодарности"
+ },
+ {
+ "id": 70,
+ "user_id": 7193750921,
+ "text": null,
+ "date": "2026-03-25T09:24:33.141258",
+ "topic": null
+ },
+ {
+ "id": 71,
+ "user_id": 1867403697,
+ "text": "Делаешь им фотку счетчика они присылают акт проверки моментально",
+ "date": "2026-03-25T09:25:12.232124",
+ "topic": "животные"
+ },
+ {
+ "id": 72,
+ "user_id": 1867403697,
+ "text": "300 р",
+ "date": "2026-03-25T09:25:22.294027",
+ "topic": null
+ },
+ {
+ "id": 73,
+ "user_id": 7193750921,
+ "text": "Цены старые, конечно.",
+ "date": "2026-03-25T09:25:22.994905",
+ "topic": null
+ },
+ {
+ "id": 74,
+ "user_id": 1670557872,
+ "text": "Ой как здорово",
+ "date": "2026-03-25T09:37:18.521901",
+ "topic": null
+ },
+ {
+ "id": 75,
+ "user_id": 1179832004,
+ "text": null,
+ "date": "2026-03-25T14:24:10.506225",
+ "topic": null
+ },
+ {
+ "id": 76,
+ "user_id": 1667052403,
+ "text": "Добрый вечер\nУ всех сегодня билайн домашний инет не работал?\n\nА теперь всё с ограничениями?",
+ "date": "2026-03-25T15:47:14.973788",
+ "topic": null
+ },
+ {
+ "id": 77,
+ "user_id": 864147545,
+ "text": "У718АС 73 хендай элантра не нужен о заезжать на «газон»(в нашем случае грязь)",
+ "date": "2026-03-25T16:59:42.057708",
+ "topic": null
+ },
+ {
+ "id": 78,
+ "user_id": 864147545,
+ "text": null,
+ "date": "2026-03-25T17:59:17.637774",
+ "topic": null
+ },
+ {
+ "id": 79,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-03-26T08:25:52.691712",
+ "topic": null
+ },
+ {
+ "id": 80,
+ "user_id": 1667052403,
+ "text": "Да да\nИнета нет\nА платим исправно....",
+ "date": "2026-03-26T08:32:56.795527",
+ "topic": null
+ },
+ {
+ "id": 81,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-03-27T15:01:39.815229",
+ "topic": null
+ },
+ {
+ "id": 82,
+ "user_id": 5036016769,
+ "text": null,
+ "date": "2026-03-27T16:54:15.376935",
+ "topic": null
+ },
+ {
+ "id": 83,
+ "user_id": 5036016769,
+ "text": "Где же вы все были когда снег там лежал",
+ "date": "2026-03-27T16:54:45.860697",
+ "topic": null
+ },
+ {
+ "id": 84,
+ "user_id": 1476737008,
+ "text": "Добрый вечер.Соседи,подскажите пожалуйста где сделать ключ от домофона",
+ "date": "2026-03-28T17:29:19.880276",
+ "topic": "соседи"
+ },
+ {
+ "id": 85,
+ "user_id": 368569899,
+ "text": "Добрый вечер. Мы пару раз делали в Лидере на 1 этаже, там где ключи делают.",
+ "date": "2026-03-28T17:37:29.003831",
+ "topic": "домофон"
+ },
+ {
+ "id": 86,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-03-29T06:10:46.673382",
+ "topic": null
+ },
+ {
+ "id": 87,
+ "user_id": 1670557872,
+ "text": ")))",
+ "date": "2026-03-29T06:12:19.450560",
+ "topic": null
+ },
+ {
+ "id": 88,
+ "user_id": 1476737008,
+ "text": "Спасибо большое)",
+ "date": "2026-03-29T06:46:04.110315",
+ "topic": "благодарности"
+ },
+ {
+ "id": 89,
+ "user_id": 1667052403,
+ "text": null,
+ "date": "2026-03-29T18:03:20.251284",
+ "topic": null
+ },
+ {
+ "id": 90,
+ "user_id": 1667052403,
+ "text": "https://ulpressa.ru/2026/03/28/upravdom-provajdery-v-mkd-2-dlya-kogo-zakony-pisany/",
+ "date": "2026-03-29T18:21:13.686290",
+ "topic": null
+ },
+ {
+ "id": 91,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-05T11:51:00.860105",
+ "topic": null
+ },
+ {
+ "id": 92,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-05T11:51:00.877190",
+ "topic": null
+ },
+ {
+ "id": 93,
+ "user_id": 1667052403,
+ "text": "К старшей и в ук \nНаверное \nСейчас концов не найдешь",
+ "date": "2026-04-05T11:51:00.903741",
+ "topic": "ук"
+ },
+ {
+ "id": 94,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-05T13:38:06.074054",
+ "topic": null
+ },
+ {
+ "id": 95,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-05T13:38:06.092502",
+ "topic": null
+ },
+ {
+ "id": 96,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-05T13:38:06.107622",
+ "topic": null
+ },
+ {
+ "id": 97,
+ "user_id": 197957361,
+ "text": "А как дела обстоят в ваших подъездах? \nЕсть такие \"дремучие\", которые никак не поймут, что с мусором делать? \n\nМожет у кого то из окна пакеты выбрасывают ? 😁",
+ "date": "2026-04-05T13:38:59.369432",
+ "topic": "уборка"
+ },
+ {
+ "id": 98,
+ "user_id": 576801733,
+ "text": "Искренне верим что завтра вывезут... я так и не поняла зачем они сложили мешки и пропали.... с кры я имею ввиду",
+ "date": "2026-04-05T13:44:27.580631",
+ "topic": null
+ },
+ {
+ "id": 99,
+ "user_id": 576801733,
+ "text": "Крыши... т9 зараза",
+ "date": "2026-04-05T13:44:46.797929",
+ "topic": null
+ },
+ {
+ "id": 100,
+ "user_id": 772234481,
+ "text": "Не всем легко эти большие баулы с мусором относить за дом,потому что они тяжелые.Я не понимаю постоянные претензии по поводу этого.(К мешкам я не причастна,но эта тема из раза в раз оговаривается в чате,зачем-не знаю,мне кажется и так все всё видят и понимают,не могу сказать точно когда,но раз в неделю приезжает машина и забирает такой мусор,можно к этому и по спокойнее относиться)\nПусть тогда все салоны/магазины/пвз и тд со стороны проспекта,куда нужно относить мусор по вашему, закроют и уберут.Потому что они закрывают второй запасной выход из подъезда.Если бы вторые запасные выходы из подъездов не были бы напичканы салонами,то мусор можно было спокойно выносить туда,на ту сторону к бакам-было бы легче и правильнее.",
+ "date": "2026-04-05T14:01:57.183265",
+ "topic": "уборка"
+ },
+ {
+ "id": 101,
+ "user_id": 197957361,
+ "text": "у нас внутри двора рекреации под это имеются\nи это отродясь не вызывало проблем никому и никогда \n\nно вот с появлением новых жильцов такое бывает, например у 5го подьезда это тупо бытовой мусор с кухни! \nнадо быть очень диким и далеким от цивилизации, чтобы выбрасывать такой мусор - под дверь подьезда",
+ "date": "2026-04-05T14:34:07.586221",
+ "topic": "уборка"
+ },
+ {
+ "id": 102,
+ "user_id": 772234481,
+ "text": "Про бытовой мусор с кухни согласна,это дикость",
+ "date": "2026-04-05T14:46:27.710436",
+ "topic": "уборка"
+ },
+ {
+ "id": 103,
+ "user_id": 772234481,
+ "text": "У 7,8,9 подъезда есть места(выход к бакам)куда можно выкидывать крупногабаритный мусор,туда уборочные машины почему-то приезжают чаще и забирают,но опять таки,когда жильцы относили мусор туда,другие жильцы из этих подъездов ругались, и высказывали своё негодование,мол зачем весь мусор относят к их подъезду",
+ "date": "2026-04-05T14:53:26.450162",
+ "topic": "уборка"
+ },
+ {
+ "id": 104,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-05T15:05:16.857463",
+ "topic": null
+ },
+ {
+ "id": 105,
+ "user_id": 1335945859,
+ "text": "Если бы увозили с такой периодичностью, чтобы не образовывалась СВАЛКА у подъезда, никто бы, я думаю, не возмущался",
+ "date": "2026-04-05T15:24:23.779266",
+ "topic": "уборка"
+ },
+ {
+ "id": 106,
+ "user_id": 1667052403,
+ "text": "Это же и про подъезды можно сказать... Увозили бы раз в неделю и разговоров бы не было\n\nКак и с уборкой подъездов и двора.\nФормулировка - по необходимости - приводит к грязи..и хаосу...",
+ "date": "2026-04-05T15:26:34.820124",
+ "topic": "уборка"
+ },
+ {
+ "id": 107,
+ "user_id": 1335945859,
+ "text": "Потому что тот кто определяет эту необходимость делает это так как считает нужным именно он (она)",
+ "date": "2026-04-05T15:31:32.428514",
+ "topic": null
+ },
+ {
+ "id": 108,
+ "user_id": 197957361,
+ "text": "А у нас разве вывоз такого мусора - никак не регламентирован ? \nВот просто не верю, по простой причине - оплата то у нас за это идет не абы как, а регулярная и постоянная.\nТак что и график там должен быть регулярный и постоянный",
+ "date": "2026-04-05T15:33:08.750641",
+ "topic": "мусор"
+ },
+ {
+ "id": 109,
+ "user_id": 576801733,
+ "text": "Уважаемые соседи, можно вопрос не про мусор, видео кто нибудь оплачивает? У меня в этом году какой то просто адский тариф и за 2 месяца 900 с лишним рублей((( подскажите плиз тариф у нас какой? А Домофон у кого нибудь вообще работает не с ключа?",
+ "date": "2026-04-05T15:33:40.638091",
+ "topic": "соседи"
+ },
+ {
+ "id": 110,
+ "user_id": 197957361,
+ "text": "Или тоже, как с камерами. Все не при чем, молиться в сторону ульяновского 2, и только по троекратному письменному разрешению неизвестно кого ? 🤣",
+ "date": "2026-04-05T15:33:54.688085",
+ "topic": null
+ },
+ {
+ "id": 111,
+ "user_id": 1667052403,
+ "text": "💯",
+ "date": "2026-04-05T15:40:06.767521",
+ "topic": null
+ },
+ {
+ "id": 112,
+ "user_id": 197957361,
+ "text": "а вот и первое сообщение о платежке в почтовом ящике - и там тариф, который не мы с вами НЕ ВЫБИРАЛИ",
+ "date": "2026-04-05T16:13:55.637380",
+ "topic": null
+ },
+ {
+ "id": 113,
+ "user_id": 197957361,
+ "text": "🔥Для жильцов нашего дома поясню\nНа прошлой неделе, благодаря инициативной группе, я побывал в отделении ОБЭП\nКоторое начало расследовать \"А что тут происходит в доме вообще?\"\nВсе эти таинственные пропадающие договора, все эти документы которых нигде нет, вся эта кутерьма с видеокамерами, вся эта ерунда с оплатой домофона самовольной по метражу, по обрезкам, по ремонтам, по уборке тракторами и метлами. \n\nУ нас за года СТОЛЬКО претензий к УО ЖСС накопилось!\n\nИ вот как то на встречу они совершенно не идут, не хотят разобраться ни в одном вопросе. Просто - в молчанку идут. Считают, что все норм, что все везде на тормозах сойдет. \n\nИ потому, нам нужна лишь платежка, как доказательства очередного их \"выкрутаса\", который совершенно не совместим ни с жилищным законодательством, ни со здравым смыслом! Взяли и сами себе тариф придумали ! Вот это выкрутасы ! \nЭто, чтобы все жильцы понимали, ну в первую очередь их зарплата например. \n\n🫡Кто нибудь может сам себе зарплату поднять на нужную ему сумму ? Просто так. В одно лицо. \nНет ? А почему интересно ? \n\nВот и мы, собственники в доме - РАБОТАДАТЕЛЬ для УО ЖСС. И сотрудник наш - почему то решил, что может сам себе зарплату назначать. \nХотелки - их, а карман то - наш. \n\nКому то это кажется правильным ? \nДля справки, со стороны закона - это нонсенс! \n\nПотому с этими их выходками конечно надо закончить. Это наглость какая то несусветная. Можно и покрепче выразиться.",
+ "date": "2026-04-05T16:13:58.099823",
+ "topic": "собрание"
+ },
+ {
+ "id": 114,
+ "user_id": 1667052403,
+ "text": "🔥🔥🔥",
+ "date": "2026-04-05T16:32:19.532647",
+ "topic": null
+ },
+ {
+ "id": 115,
+ "user_id": 1179832004,
+ "text": "Доброе утро,9 подъезд 3-ка нет воды,что случилось?",
+ "date": "2026-04-06T05:21:23.714970",
+ "topic": "уборка"
+ },
+ {
+ "id": 116,
+ "user_id": 5235580033,
+ "text": "Щенка никто не терял 6 подъезд, предположительно хаски",
+ "date": "2026-04-07T15:59:12.269686",
+ "topic": "уборка"
+ },
+ {
+ "id": 117,
+ "user_id": 5246728667,
+ "text": "Здравствуйте, а в чем проблема вторую ночь подряд нет напора холодной воды? Днем всё хорошо, а ночью холодная вода еле еле течёт.",
+ "date": "2026-04-07T21:13:03.405086",
+ "topic": "вода"
+ },
+ {
+ "id": 118,
+ "user_id": 458413170,
+ "text": "Добрый день. Пришел счёт в приложение банка. Скажите, это что вообще",
+ "date": "2026-04-09T14:09:47.993980",
+ "topic": null
+ },
+ {
+ "id": 119,
+ "user_id": 458413170,
+ "text": null,
+ "date": "2026-04-09T14:10:04.260985",
+ "topic": null
+ },
+ {
+ "id": 120,
+ "user_id": 458413170,
+ "text": null,
+ "date": "2026-04-09T14:10:04.284659",
+ "topic": null
+ },
+ {
+ "id": 121,
+ "user_id": 462572897,
+ "text": "Возможно это капремонт. За несколько месяцев, если не платили ...",
+ "date": "2026-04-09T14:14:34.659087",
+ "topic": "соседи"
+ },
+ {
+ "id": 122,
+ "user_id": 458413170,
+ "text": "Так за капремонт разве мы не оплачиваем ежемесячно в РИЦ?",
+ "date": "2026-04-09T14:20:25.906081",
+ "topic": "соседи"
+ },
+ {
+ "id": 123,
+ "user_id": 458413170,
+ "text": "Я так понимаю, оплачиваем все",
+ "date": "2026-04-09T14:55:28.518224",
+ "topic": null
+ },
+ {
+ "id": 124,
+ "user_id": 458413170,
+ "text": null,
+ "date": "2026-04-09T14:55:33.063509",
+ "topic": null
+ },
+ {
+ "id": 125,
+ "user_id": 8181748817,
+ "text": "В этой платежке указано что платеж за кап.ремонт за март 26г. (при цене 15,05 руб за кв.м и площади квартиры 66,8 м.кв). \nОплата проводится ежемесячно через РИЦ на спецсчет дома.\nВсе, больше ничего по кап.ремонту платить не нужно.\n\nНикаких прямых платежей в фонд кап.ремонта не должно быть. Счет на 4820 руб - возможно от мошеников, будьте аккуратнее, никаких действий не производите. \n\nОбратитесь в банк, от кого выставлен счет, там выявят, если от мошеников.",
+ "date": "2026-04-09T19:27:30.222466",
+ "topic": "соседи"
+ },
+ {
+ "id": 126,
+ "user_id": 8181748817,
+ "text": "В этой платежке указано что платеж за кап.ремонт за март 26г. (при цене 15,05 руб за кв.м и площади квартиры 66,8 м.кв). \nОплата проводится ежемесячно через РИЦ на спецсчет дома.\nВсе, больше ничего по кап.ремонту платить не нужно.\n\nНикаких прямых платежей в фонд кап.ремонта не должно быть. Счет на 4820 руб - возможно от мошеников, будьте аккуратнее, никаких действий не производите. \n\nОбратитесь в банк, от кого выставлен счет, там выявят, если от мошеников.",
+ "date": "2026-04-09T19:32:58.749051",
+ "topic": "соседи"
+ },
+ {
+ "id": 127,
+ "user_id": 1335945859,
+ "text": "Раньше я боялся темноты, но вчера принесли квитанции. Теперь я боюсь света, воды и тепла. И еще немного — мусора.",
+ "date": "2026-04-10T15:16:15.029712",
+ "topic": "уборка"
+ },
+ {
+ "id": 128,
+ "user_id": 7346013939,
+ "text": "Добрый день! Как наши успехи, деньги собрали на пошлину, теперь проблема с телегой. Информации мало.",
+ "date": "2026-04-11T08:03:52.949041",
+ "topic": null
+ },
+ {
+ "id": 129,
+ "user_id": 1353946650,
+ "text": null,
+ "date": "2026-04-11T12:03:02.956710",
+ "topic": null
+ },
+ {
+ "id": 130,
+ "user_id": 5183810334,
+ "text": null,
+ "date": "2026-04-12T04:35:40.077555",
+ "topic": null
+ },
+ {
+ "id": 131,
+ "user_id": 2139751123,
+ "text": "ВО ИСТИНУ ВОСКРЕСЕ!!!\nХРИСТОС ВОСКРЕСЕ!!!\nХРИСТОС ВОСКРЕСЕ!!!\nХРИСТОС ВОСКРЕСЕ!!!",
+ "date": "2026-04-12T06:32:17.398539",
+ "topic": null
+ },
+ {
+ "id": 132,
+ "user_id": 1667052403,
+ "text": null,
+ "date": "2026-04-12T06:49:15.228600",
+ "topic": null
+ },
+ {
+ "id": 133,
+ "user_id": 5183810334,
+ "text": null,
+ "date": "2026-04-12T06:57:10.820177",
+ "topic": null
+ },
+ {
+ "id": 134,
+ "user_id": 8181748817,
+ "text": "🙏🙏🌞💐",
+ "date": "2026-04-12T07:16:38.356136",
+ "topic": null
+ },
+ {
+ "id": 135,
+ "user_id": 8181748817,
+ "text": "🙏🙏🌞💐",
+ "date": "2026-04-12T07:16:38.400835",
+ "topic": null
+ },
+ {
+ "id": 136,
+ "user_id": 576801733,
+ "text": "ВОИСТИНУ ВОСКРЕСЕ!!!!!🙏🙏💐🌷🌹",
+ "date": "2026-04-12T07:41:00.515836",
+ "topic": null
+ },
+ {
+ "id": 137,
+ "user_id": 1670557872,
+ "text": null,
+ "date": "2026-04-12T08:04:05.021389",
+ "topic": null
+ },
+ {
+ "id": 138,
+ "user_id": 5106504485,
+ "text": null,
+ "date": "2026-04-12T10:36:28.845060",
+ "topic": null
+ },
+ {
+ "id": 139,
+ "user_id": 5106504485,
+ "text": null,
+ "date": "2026-04-12T10:36:50.775138",
+ "topic": null
+ },
+ {
+ "id": 140,
+ "user_id": 5106504485,
+ "text": "1подъезд🫣",
+ "date": "2026-04-12T10:36:53.349640",
+ "topic": "уборка"
+ },
+ {
+ "id": 141,
+ "user_id": 1970561536,
+ "text": "Детки голодные,ели сразу",
+ "date": "2026-04-12T10:38:08.866439",
+ "topic": null
+ },
+ {
+ "id": 142,
+ "user_id": 5106504485,
+ "text": "Это они не ели,а раскидали на три этажа",
+ "date": "2026-04-12T10:39:51.117025",
+ "topic": null
+ },
+ {
+ "id": 143,
+ "user_id": 462572897,
+ "text": "Не воспитанные, родители взрастили вседозволенность. Хоть праздник, хоть что ... Дикость.",
+ "date": "2026-04-12T14:05:15.606018",
+ "topic": null
+ },
+ {
+ "id": 144,
+ "user_id": 197957361,
+ "text": "Не пойму никак, яйца то при чем - вроде день Гагарина сегодня ? не ? \nВпрочем это лирика ! \nА что у нас по гамбургскому счету ?",
+ "date": "2026-04-12T14:13:52.405574",
+ "topic": null
+ },
+ {
+ "id": 145,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-12T14:14:15.163391",
+ "topic": null
+ },
+ {
+ "id": 146,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-12T14:14:26.193642",
+ "topic": null
+ },
+ {
+ "id": 147,
+ "user_id": 197957361,
+ "text": "📌 Мы тут с инициативными жильцами обсуждали буквально на днях, что пора бы подавать в суд, иском прям, на нашу \"любимую\" управляющую организацию УО ЖСС, и ее \"опытного бухгалтера\" по совместительству председателя текущего нашего дома - гражданку Ерееву Т.М. \nИ знаете, таки иск в суд отправлен ! \nНу а раз формальные правила требуют от нас отправки копии иска - в адрес Ответчика (УО ЖСС), то им оно ушло первым делом. \n\nТеперь в понедельник и УО ЖСС, и суд заволжский, получат иски\nГде черным по белому будет написано кое что интересное из жизни нашей УО ЖСС в плане \"обмишуривания\" \"обдуривания\" нашего дома в плане расчетов и тарифов, и всех событий первых трех месяцев 2026 года. \n\nОгромное спасибо нашим юристам, огромное спасибо нашим экономистам, огромное спасибо всем кто поучаствовал в сборе 💰 на госпошлину для суда. Все собралось, все написалось, все факты и замечания учтены, иски ушли и теперь нам осталось лишь подождать что нам суд на это ответит. \n\nps я не юрист, но прочитав все, мне самому стало интересно, можно ли используя юридические инсинуации и всякие там \"я с Петровичем на рыбалку езжу\" умудриться в суде нормами юридическими действующего законодательства победить законы физики и геометрии нашей вселенной?? В силу образования направленного на ✈️авиастроительство - не вижу ни единого такого выхода.",
+ "date": "2026-04-12T14:20:35.418521",
+ "topic": "ук"
+ },
+ {
+ "id": 148,
+ "user_id": 1667052403,
+ "text": "Спасибо Александр",
+ "date": "2026-04-12T15:12:27.802314",
+ "topic": "благодарности"
+ },
+ {
+ "id": 149,
+ "user_id": 197957361,
+ "text": "Погоды не позволяют пока что беседы неспешные во дворе проводить \nНо вот за неделю с несколькими жильцами я тут повстречался, минут по сорок-часу побеседовали\n\nГлаза квадратные у людей от происходящего, шашками хотят махать, писать и прочее\n\nВот уж где инициативная группа поработает весною теплою ! Вот где люди прозреют",
+ "date": "2026-04-12T15:14:13.060243",
+ "topic": "вода"
+ },
+ {
+ "id": 150,
+ "user_id": 259925158,
+ "text": null,
+ "date": "2026-04-12T17:50:38.219022",
+ "topic": null
+ },
+ {
+ "id": 151,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-13T08:35:21.162041",
+ "topic": null
+ },
+ {
+ "id": 152,
+ "user_id": 576801733,
+ "text": "Так нам отказали? Что то я запуталась",
+ "date": "2026-04-13T18:41:44.278998",
+ "topic": null
+ },
+ {
+ "id": 153,
+ "user_id": 5246728667,
+ "text": "Здравствуйте, а почему иск подан только от одного жителя, а не от всех? Это было бы не эффективней?",
+ "date": "2026-04-13T18:57:42.401189",
+ "topic": null
+ },
+ {
+ "id": 154,
+ "user_id": 5246728667,
+ "text": "Рассмотреть дело в его отсутствии, а доказательная база там какая-то приложена? Фото раслейки объявлений, скрины и тд..",
+ "date": "2026-04-13T19:02:18.819016",
+ "topic": "объявления"
+ },
+ {
+ "id": 155,
+ "user_id": 197957361,
+ "text": "Было бы конечно\nТолько тогда каждый кто хотел бы там присутствовать - должен бы был сделать нотариальную доверенность на того, кто подает сам иск, ну и составить список-реестр всех таких желающих. \n\nОдин иск, в котором было написано что он коллективный - недавно по формальным признакам - суд спокойно развернул. Оказывается коли он коллективный - надо чтобы его подавал человек с дипломом юриста. \nУ нас в доме есть дипломированные юристы, но что то не горят желанием отправлять такие иски. \nНу и снова, нотариальные доверенности от каждого жителя - это поход каждого желающего к адвокату, и сумма денег за заверение. \n\nТак что выбор то есть, но видишь, какой он странный этот выбор. \n\nНо ничего страшного, юристы же все пояснили. Само решение суда - будет одинаковое, что иск от одного человека, что от двести пятьдесят восьми.",
+ "date": "2026-04-13T19:03:16.995010",
+ "topic": null
+ },
+ {
+ "id": 156,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-13T19:04:15.880027",
+ "topic": null
+ },
+ {
+ "id": 157,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-13T19:04:15.899573",
+ "topic": null
+ },
+ {
+ "id": 158,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-13T19:04:15.914759",
+ "topic": null
+ },
+ {
+ "id": 159,
+ "user_id": 197957361,
+ "text": "База у нас там чуть ли не на полкилограмма\nСуду что надо будет еще - запросят, без проблем предоставим\n\nПущай вон ОБЭП запрашивают, следователь кстати сразу сказал, что когда иск пойдет - так и написать, он все предоставит",
+ "date": "2026-04-13T19:05:05.470710",
+ "topic": "уборка"
+ },
+ {
+ "id": 160,
+ "user_id": 5246728667,
+ "text": "Надеюсь всё получится 💪",
+ "date": "2026-04-13T19:16:41.766248",
+ "topic": "уборка"
+ },
+ {
+ "id": 161,
+ "user_id": 197957361,
+ "text": "Конечно получится !",
+ "date": "2026-04-13T19:17:22.675385",
+ "topic": "уборка"
+ },
+ {
+ "id": 162,
+ "user_id": 1670557872,
+ "text": "Вот вообще не удивлюсь, что каким-то образом суд тоже не увидит правонарушений.... И скажет что ЖСС наш молодец. Что-то куда Саша не обращается, везде абсолютно отписки",
+ "date": "2026-04-14T04:16:50.938317",
+ "topic": "благодарности"
+ },
+ {
+ "id": 163,
+ "user_id": 1335945859,
+ "text": "Надо верить в лучшее) все будет хорошо)",
+ "date": "2026-04-14T04:29:58.430410",
+ "topic": "благодарности"
+ },
+ {
+ "id": 164,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-14T06:25:17.741926",
+ "topic": null
+ },
+ {
+ "id": 165,
+ "user_id": 197957361,
+ "text": "https://www.rupor73.ru/obshchestvo/45208-karlinu-postavili-neud-za-rabotu-gosstrojzhilnadzora",
+ "date": "2026-04-15T02:10:12.122316",
+ "topic": null
+ },
+ {
+ "id": 166,
+ "user_id": 1667052403,
+ "text": "⚡️ Российские власти могут повременить с ужесточением контроля над интернетом, особенно в отношении Telegram \n\nПричиной стали опасения, что это сказывается на рейтинге в связи с выборами, сообщили источники Bloomberg.\n\n😱 Подписаться | Прислать новость",
+ "date": "2026-04-15T04:15:28.556000",
+ "topic": null
+ },
+ {
+ "id": 167,
+ "user_id": 1670557872,
+ "text": "Только в связи с выборами!)",
+ "date": "2026-04-15T04:48:02.097930",
+ "topic": null
+ },
+ {
+ "id": 168,
+ "user_id": 2139751123,
+ "text": "\"Вор должен сидеть в тюрьме...\", но мафия бессмертна 🤔",
+ "date": "2026-04-15T06:18:46.540146",
+ "topic": null
+ },
+ {
+ "id": 169,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-17T04:14:14.598897",
+ "topic": null
+ },
+ {
+ "id": 170,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-17T13:11:10.664133",
+ "topic": null
+ }
+ ]
+}
\ No newline at end of file
diff --git a/exports/chat_history_2026-04-18_23-21.json b/exports/chat_history_2026-04-18_23-21.json
new file mode 100644
index 0000000..bd4eb23
--- /dev/null
+++ b/exports/chat_history_2026-04-18_23-21.json
@@ -0,0 +1,1474 @@
+{
+ "info": {
+ "exported_at": "2026-04-18T23:21:37.142340+04:00",
+ "total_users": 39,
+ "total_messages": 170
+ },
+ "users": [
+ {
+ "id": 197957361,
+ "username": "ktybdsq",
+ "name": "Александр MatrixHasYou",
+ "apt": "152",
+ "verified": true
+ },
+ {
+ "id": 5475262081,
+ "username": null,
+ "name": "Надежда Тырина",
+ "apt": "152",
+ "verified": true
+ },
+ {
+ "id": 8725618164,
+ "username": "lkmthreeseven_bot",
+ "name": "DomovoyKontrolBot",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 8181748817,
+ "username": null,
+ "name": "Фаина",
+ "apt": "186",
+ "verified": true
+ },
+ {
+ "id": 1867403697,
+ "username": "cvetok_nura",
+ "name": "Светлана",
+ "apt": "171",
+ "verified": true
+ },
+ {
+ "id": 1335945859,
+ "username": null,
+ "name": "O4aPoBaHHblu_cTpaHHuK",
+ "apt": "",
+ "verified": true
+ },
+ {
+ "id": 1299551170,
+ "username": "katt_rin9",
+ "name": "Екатерина🐈",
+ "apt": "205",
+ "verified": true
+ },
+ {
+ "id": 1970561536,
+ "username": null,
+ "name": "Окся",
+ "apt": "255 КВ",
+ "verified": true
+ },
+ {
+ "id": 576801733,
+ "username": "VolkovaElena73",
+ "name": "Елена Волкова Ульяновск",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 1136316705,
+ "username": "IrinaTs15",
+ "name": "Irina Tsyganova",
+ "apt": "319",
+ "verified": true
+ },
+ {
+ "id": 462572897,
+ "username": null,
+ "name": "Алекс",
+ "apt": "48",
+ "verified": true
+ },
+ {
+ "id": 259925158,
+ "username": "Izuma_net",
+ "name": "izuma_net",
+ "apt": "295",
+ "verified": true
+ },
+ {
+ "id": 861072224,
+ "username": "AFadeeva",
+ "name": "Александра🌤",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 1670557872,
+ "username": "KhaykinaAnastasia",
+ "name": "Анастасия",
+ "apt": "295",
+ "verified": true
+ },
+ {
+ "id": 1179832004,
+ "username": "st5ksu",
+ "name": "Kseniya Stepanova",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 1432214116,
+ "username": "pro100beze",
+ "name": "Алёна",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 1861008160,
+ "username": null,
+ "name": "Елена",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 2139751123,
+ "username": null,
+ "name": "Евгений Петрович",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 5246728667,
+ "username": "valeriya_nagini",
+ "name": "Валерия",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 7193750921,
+ "username": null,
+ "name": "Светлана",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 7339698511,
+ "username": null,
+ "name": "Елена",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 368569899,
+ "username": "Countesss",
+ "name": "Анастасия Лютерова",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 6344554911,
+ "username": null,
+ "name": "Наташа 🌹",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 5166131650,
+ "username": null,
+ "name": "Николай Петрович",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 1667052403,
+ "username": null,
+ "name": "Ольга",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 5167606723,
+ "username": null,
+ "name": "Tatiyana",
+ "apt": "297",
+ "verified": true
+ },
+ {
+ "id": 5183810334,
+ "username": null,
+ "name": "Леонид",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 2044370304,
+ "username": "Brasletvz73",
+ "name": "Браслет",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 6654418281,
+ "username": "snez_frolova99",
+ "name": "Твоя ногтевая фея💅🏼",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 864147545,
+ "username": "roman_garipov",
+ "name": "Roman",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 5036016769,
+ "username": null,
+ "name": "Алсу Юсупова",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 1476737008,
+ "username": null,
+ "name": "Елена",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 772234481,
+ "username": null,
+ "name": "Анастасия",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 5235580033,
+ "username": null,
+ "name": "Александр",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 458413170,
+ "username": "MeLnikov_Alesha",
+ "name": "Алексей Мельников",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 7346013939,
+ "username": null,
+ "name": "Любовь Землянская",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 1353946650,
+ "username": null,
+ "name": "ГеннадийП",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 5106504485,
+ "username": null,
+ "name": "Ирина",
+ "apt": null,
+ "verified": true
+ },
+ {
+ "id": 7702466390,
+ "username": "Ulik8319",
+ "name": "Юлия",
+ "apt": "287",
+ "verified": true
+ }
+ ],
+ "messages": [
+ {
+ "id": 1,
+ "user_id": 8181748817,
+ "text": "Это отчет УО за 2025г.\nТатьяна Мих. расклеила",
+ "date": "2026-03-11T17:06:13.912964",
+ "topic": null
+ },
+ {
+ "id": 2,
+ "user_id": 197957361,
+ "text": "Такая куча ссылок на какие то акты - да с номерами !\nФаина явно заинтересует вам что то из актов !",
+ "date": "2026-03-11T17:16:35.705412",
+ "topic": null
+ },
+ {
+ "id": 3,
+ "user_id": 197957361,
+ "text": "162 квартира, 5й подьезд - топят всех \n\nкто нибудь знает кто живет там в 2шке ?",
+ "date": "2026-03-11T18:27:49.681833",
+ "topic": null
+ },
+ {
+ "id": 4,
+ "user_id": 197957361,
+ "text": "аварийку вызвали, перекрыли воду и уехали \nдверь никто не открывает, никого нету",
+ "date": "2026-03-11T18:49:43.652398",
+ "topic": "вода"
+ },
+ {
+ "id": 5,
+ "user_id": 197957361,
+ "text": "❔Кто нибудь в нашем доме понимает - каким таким образом ЖСС считает, что бумажка без подписи и печати на подьезде позволяет им навязать нам, собственникам, какой то выдуманный тариф с марта ? \n\nУже сколько они тарифов предлагали нам ? со счета сбился я😆\nВсе разные\nНи один не обоснован ничем\n\nИ вот - теперь вообще спустя рукава, бумажку из принтера повесили - вот вам новый тариф! \n\nЧто то нету таких вариантов в Жилищном Кодексе РФ, где четко написано, что этот вопрос решается только на Общем Собрании Собственников путем голосования\n\nАх да! на бумажке же ссылка на какой то договор управления домом от 2015 года... мммм. \nА кто нибудь его открывал ? \nДоговор №65? он в ГИС ЖКХ висит у всех\n\n✅Договор № 65 от 01.12.2015 года подписан со стороны «собственника» председателем Комитета ЖКХ администрации г. Ульяновска Ворожецовым А.В.\n\nСогласно ч. 1 ст. 162 ЖК РФ, договор управления заключается с каждым собственником в письменной форме\n. Чиновник Ворожецов не имел права подписывать условия (включая автоматическую индексацию) за частных собственников приватизированных квартир\n\nСсылка ЖСС на этот договор как на основание для индексации в 2026 году — это попытка распространить условия сделки, совершенной третьими лицами (администрацией), на жителей дома без их согласия.\n\n\n✅Факты: В Протоколе № Лен.Ком.37/1 от 29.01.2025 года собственники уже реализовали свое право по ст. 156 ЖК РФ и утвердили конкретный тариф — 37,50 руб./м² на период до 31.01.2026 года\n.\nНарушение: ЖСС в своем уведомлении о росте тарифа до 40,80 руб.\n полностью игнорирует тот факт, что механизм индексации из «старого» договора (п. 3.6)\n был «убит» принятием конкретного решения собственников в 2025 году. Согласно ч. 7 ст. 156 ЖК РФ, размер платы устанавливается только на общем собрании\n. Раз жители в 2025 году установили твердую цифру, никакая «автоматическая индексация» больше не работает.",
+ "date": "2026-03-12T17:02:51.184428",
+ "topic": "собрание"
+ },
+ {
+ "id": 6,
+ "user_id": 197957361,
+ "text": "Вот смеху то будет, если в апреле такая платежка прийдет ! 😃\n\nКак только придет первая платежка с тарифом выше 37,50 руб., подаем массовые требования о выплате штрафа 50% от суммы превышения на основании ч. 11 ст. 156 ЖК РФ\n\nНу, кто хочет платить по выдуманному тарифу может конечно при своем остаться, у нас же свобода воли полная (но не по мнению ЖСС 😁)",
+ "date": "2026-03-12T17:04:16.699206",
+ "topic": "уборка"
+ },
+ {
+ "id": 7,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-03-17T11:44:28.416187",
+ "topic": null
+ },
+ {
+ "id": 8,
+ "user_id": 8181748817,
+ "text": "Смешной штраф🙈",
+ "date": "2026-03-17T11:44:28.638147",
+ "topic": null
+ },
+ {
+ "id": 9,
+ "user_id": 576801733,
+ "text": "Спасибки, завтра доеду)))",
+ "date": "2026-03-17T11:44:28.723239",
+ "topic": "благодарности"
+ },
+ {
+ "id": 10,
+ "user_id": 1179832004,
+ "text": "Значит вам нужно на Киевский,они установят вам приложение",
+ "date": "2026-03-17T11:44:28.742319",
+ "topic": null
+ },
+ {
+ "id": 11,
+ "user_id": 1432214116,
+ "text": null,
+ "date": "2026-03-17T11:44:28.763231",
+ "topic": null
+ },
+ {
+ "id": 12,
+ "user_id": 576801733,
+ "text": "Он выдаёт мне на андроид опять vdome, а оно не пашет((((",
+ "date": "2026-03-17T11:44:28.863145",
+ "topic": null
+ },
+ {
+ "id": 13,
+ "user_id": 1670557872,
+ "text": "Понятно, спасибо",
+ "date": "2026-03-17T11:44:28.903963",
+ "topic": "благодарности"
+ },
+ {
+ "id": 14,
+ "user_id": 1861008160,
+ "text": null,
+ "date": "2026-03-17T11:44:28.923567",
+ "topic": null
+ },
+ {
+ "id": 15,
+ "user_id": 5246728667,
+ "text": "Забрали уже.",
+ "date": "2026-03-17T11:44:28.952646",
+ "topic": null
+ },
+ {
+ "id": 16,
+ "user_id": 2139751123,
+ "text": "Отписка пять баллов 👍",
+ "date": "2026-03-17T11:44:28.974797",
+ "topic": null
+ },
+ {
+ "id": 17,
+ "user_id": 1299551170,
+ "text": "Через госуслуги попробуйте связаться",
+ "date": "2026-03-17T11:44:29.175932",
+ "topic": null
+ },
+ {
+ "id": 18,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-03-17T17:59:33.059721",
+ "topic": null
+ },
+ {
+ "id": 19,
+ "user_id": 7339698511,
+ "text": "Люди!!! Дайте ссылки на домофон и на камеры???",
+ "date": "2026-03-19T05:28:21.047935",
+ "topic": "домофон"
+ },
+ {
+ "id": 20,
+ "user_id": 7339698511,
+ "text": "Или напишите адреса куда можно обратиться по поводу камер и домофона, пожалуйста",
+ "date": "2026-03-19T05:29:12.037940",
+ "topic": "вода"
+ },
+ {
+ "id": 21,
+ "user_id": 368569899,
+ "text": "Елена, я вам написала в личные сообщения",
+ "date": "2026-03-19T06:07:28.399172",
+ "topic": null
+ },
+ {
+ "id": 22,
+ "user_id": 7339698511,
+ "text": "Спасибо большое",
+ "date": "2026-03-19T06:17:47.232121",
+ "topic": "благодарности"
+ },
+ {
+ "id": 23,
+ "user_id": 6344554911,
+ "text": "Здравствуйте. Кто знает телефон нашего сантехника, дайте пожалуйста",
+ "date": "2026-03-19T12:26:07.174734",
+ "topic": "вопросы"
+ },
+ {
+ "id": 24,
+ "user_id": 259925158,
+ "text": "Попробуйте этот 8 (996) 219-95-67",
+ "date": "2026-03-19T12:45:37.451726",
+ "topic": null
+ },
+ {
+ "id": 25,
+ "user_id": 6344554911,
+ "text": "Спасибо",
+ "date": "2026-03-19T12:48:10.923728",
+ "topic": "благодарности"
+ },
+ {
+ "id": 26,
+ "user_id": 197957361,
+ "text": "А как его зовут ? Может его в бота в раздел Мастера записать ?",
+ "date": "2026-03-19T15:28:58.258357",
+ "topic": null
+ },
+ {
+ "id": 27,
+ "user_id": 6344554911,
+ "text": "Его Алексей зовут",
+ "date": "2026-03-19T15:32:23.718845",
+ "topic": null
+ },
+ {
+ "id": 28,
+ "user_id": 5166131650,
+ "text": "Напротив 5 подъезда?",
+ "date": "2026-03-20T11:35:38.550863",
+ "topic": "уборка"
+ },
+ {
+ "id": 29,
+ "user_id": 5166131650,
+ "text": "Напротив 6 подъезда?",
+ "date": "2026-03-20T11:37:13.711268",
+ "topic": "уборка"
+ },
+ {
+ "id": 30,
+ "user_id": 1667052403,
+ "text": "Вы о чём?",
+ "date": "2026-03-20T11:47:26.526343",
+ "topic": null
+ },
+ {
+ "id": 31,
+ "user_id": 8181748817,
+ "text": null,
+ "date": "2026-03-20T14:14:46.056946",
+ "topic": null
+ },
+ {
+ "id": 32,
+ "user_id": 8181748817,
+ "text": null,
+ "date": "2026-03-20T14:14:46.089582",
+ "topic": null
+ },
+ {
+ "id": 33,
+ "user_id": 8181748817,
+ "text": null,
+ "date": "2026-03-20T14:14:46.113065",
+ "topic": null
+ },
+ {
+ "id": 34,
+ "user_id": 8181748817,
+ "text": null,
+ "date": "2026-03-20T14:14:46.136436",
+ "topic": null
+ },
+ {
+ "id": 35,
+ "user_id": 8181748817,
+ "text": null,
+ "date": "2026-03-20T14:14:46.161576",
+ "topic": null
+ },
+ {
+ "id": 36,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-03-21T07:45:12.249018",
+ "topic": null
+ },
+ {
+ "id": 37,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-03-21T07:47:26.209479",
+ "topic": null
+ },
+ {
+ "id": 38,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-03-21T09:36:37.219173",
+ "topic": null
+ },
+ {
+ "id": 39,
+ "user_id": 8181748817,
+ "text": null,
+ "date": "2026-03-21T10:40:53.080101",
+ "topic": null
+ },
+ {
+ "id": 40,
+ "user_id": 8181748817,
+ "text": null,
+ "date": "2026-03-21T10:40:53.117680",
+ "topic": null
+ },
+ {
+ "id": 41,
+ "user_id": 8181748817,
+ "text": null,
+ "date": "2026-03-21T10:40:53.162184",
+ "topic": null
+ },
+ {
+ "id": 42,
+ "user_id": 8181748817,
+ "text": "Вот договор, который нам дали на руки в УО ЖСС 27 февраля. В ГИС ЖКХ его нет.\nПо п.4.1.3. сказано, что запись из архива предоставляется по письменным запросам собственников МКД непосредственно в ООО \"ВидеоСервис\".\nПо п.2.2. записано, что Акты выполнкнеых работ подписывает Исполнитель, Заквзчик и ПСДома, на основании которых ежемесячно производится оплата.",
+ "date": "2026-03-21T10:50:22.689484",
+ "topic": "ук"
+ },
+ {
+ "id": 43,
+ "user_id": 8181748817,
+ "text": "Жители дома при инцидентах обращались к Т.Мих, так как договора никто ранее и не видел, кроме ПСдома.",
+ "date": "2026-03-21T10:54:32.497882",
+ "topic": null
+ },
+ {
+ "id": 44,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-03-21T10:57:54.609831",
+ "topic": null
+ },
+ {
+ "id": 45,
+ "user_id": 8181748817,
+ "text": "Если с января 26г услуга не оказывается, почему Акты ПСДома подписывает. \nЯ лично за янв- февр.за камеры не плачу.",
+ "date": "2026-03-21T10:58:35.386487",
+ "topic": null
+ },
+ {
+ "id": 46,
+ "user_id": 197957361,
+ "text": "Какой же нормальный человек будет в магазине покупая диван, заключать договор не между собой и магазином - а подписывать какой то договор между Магазином и другим человеком ? \nОн потом как в суд пойдет если что случится ? \nВ суде пальцем у виска покрутят и скажут \"Это ж не ваш договор, чего вы на него ссылаетесь ? \"",
+ "date": "2026-03-21T10:58:50.056137",
+ "topic": "домофон"
+ },
+ {
+ "id": 47,
+ "user_id": 197957361,
+ "text": "да ПСД дома подпишет все, что ей ЖСС даст, не глядя и забыв про бухгалтерию 😀😂",
+ "date": "2026-03-21T10:59:18.563614",
+ "topic": null
+ },
+ {
+ "id": 48,
+ "user_id": 8181748817,
+ "text": "Там приложено Доп.согл.11 от 01.02.24г, которым наш дом присоединился",
+ "date": "2026-03-21T11:00:57.340571",
+ "topic": null
+ },
+ {
+ "id": 49,
+ "user_id": 462572897,
+ "text": "Фаина, а как понять, что с января 26 г. услуга не оказывается?",
+ "date": "2026-03-21T11:50:50.112523",
+ "topic": null
+ },
+ {
+ "id": 50,
+ "user_id": 8181748817,
+ "text": "Я пишу: \"если с января...\" так как в чате жители писали, что не могут получить доступ к видео.\nИ в январе исполнитель с Видеосервис говорил, что с января 26г.доступ к архиву с разрешения Погодина.",
+ "date": "2026-03-21T12:04:13.443501",
+ "topic": "уборка"
+ },
+ {
+ "id": 51,
+ "user_id": 8181748817,
+ "text": "Уважаемые жители дома!\nКто в янв- февр- марте обращался за доступом к видеоархиву и получил его? А кто не получил?",
+ "date": "2026-03-21T12:07:50.673653",
+ "topic": "уборка"
+ },
+ {
+ "id": 52,
+ "user_id": 197957361,
+ "text": "А за уборкой подьезда и заменой лампочек в подьезде - видимо тоже куда то надо обращаться ? \n\nУ меня ощущение у одного, что ЖСС как бы и не при чем ? \n\nНу старается показать что не при чем 😅",
+ "date": "2026-03-21T12:10:14.825495",
+ "topic": "уборка"
+ },
+ {
+ "id": 53,
+ "user_id": 1179832004,
+ "text": null,
+ "date": "2026-03-21T19:25:04.858395",
+ "topic": null
+ },
+ {
+ "id": 54,
+ "user_id": 5183810334,
+ "text": "Вы что уличный сторож?",
+ "date": "2026-03-21T21:27:31.050046",
+ "topic": null
+ },
+ {
+ "id": 55,
+ "user_id": 1179832004,
+ "text": "А вы наверное юморист?",
+ "date": "2026-03-22T04:46:48.764565",
+ "topic": null
+ },
+ {
+ "id": 56,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-03-22T11:38:52.025450",
+ "topic": null
+ },
+ {
+ "id": 57,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-03-22T11:38:52.034326",
+ "topic": null
+ },
+ {
+ "id": 58,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-03-23T10:04:38.017104",
+ "topic": null
+ },
+ {
+ "id": 59,
+ "user_id": 2044370304,
+ "text": null,
+ "date": "2026-03-24T14:50:38.299476",
+ "topic": null
+ },
+ {
+ "id": 60,
+ "user_id": 2044370304,
+ "text": "Товарищи, кто знает хозяина этой машины ?",
+ "date": "2026-03-24T14:53:36.062481",
+ "topic": "вопросы"
+ },
+ {
+ "id": 61,
+ "user_id": 2044370304,
+ "text": null,
+ "date": "2026-03-24T14:53:40.014838",
+ "topic": null
+ },
+ {
+ "id": 62,
+ "user_id": 6654418281,
+ "text": "Эвакуатор может вызвать ?",
+ "date": "2026-03-24T15:02:20.242206",
+ "topic": null
+ },
+ {
+ "id": 63,
+ "user_id": 1667052403,
+ "text": "Пишите в чат 5 дома. Может там хозяин",
+ "date": "2026-03-24T15:05:27.186789",
+ "topic": null
+ },
+ {
+ "id": 64,
+ "user_id": 2044370304,
+ "text": "Как в их чат написать ?",
+ "date": "2026-03-24T15:15:45.859199",
+ "topic": null
+ },
+ {
+ "id": 65,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-03-25T08:25:50.145257",
+ "topic": null
+ },
+ {
+ "id": 66,
+ "user_id": 1670557872,
+ "text": "Добрый день! Подскажите пожалуйста, кто-нибудь знает номер телефона организации по поверке счетчиков",
+ "date": "2026-03-25T09:22:49.504050",
+ "topic": "вопросы"
+ },
+ {
+ "id": 67,
+ "user_id": 1867403697,
+ "text": "+7 927 270-21-70 добрый день",
+ "date": "2026-03-25T09:23:59.518450",
+ "topic": null
+ },
+ {
+ "id": 68,
+ "user_id": 1867403697,
+ "text": "Мне вот они понравились )",
+ "date": "2026-03-25T09:24:06.325785",
+ "topic": null
+ },
+ {
+ "id": 69,
+ "user_id": 1670557872,
+ "text": "Большое спасибо!",
+ "date": "2026-03-25T09:24:23.524322",
+ "topic": "благодарности"
+ },
+ {
+ "id": 70,
+ "user_id": 7193750921,
+ "text": null,
+ "date": "2026-03-25T09:24:33.141258",
+ "topic": null
+ },
+ {
+ "id": 71,
+ "user_id": 1867403697,
+ "text": "Делаешь им фотку счетчика они присылают акт проверки моментально",
+ "date": "2026-03-25T09:25:12.232124",
+ "topic": "животные"
+ },
+ {
+ "id": 72,
+ "user_id": 1867403697,
+ "text": "300 р",
+ "date": "2026-03-25T09:25:22.294027",
+ "topic": null
+ },
+ {
+ "id": 73,
+ "user_id": 7193750921,
+ "text": "Цены старые, конечно.",
+ "date": "2026-03-25T09:25:22.994905",
+ "topic": null
+ },
+ {
+ "id": 74,
+ "user_id": 1670557872,
+ "text": "Ой как здорово",
+ "date": "2026-03-25T09:37:18.521901",
+ "topic": null
+ },
+ {
+ "id": 75,
+ "user_id": 1179832004,
+ "text": null,
+ "date": "2026-03-25T14:24:10.506225",
+ "topic": null
+ },
+ {
+ "id": 76,
+ "user_id": 1667052403,
+ "text": "Добрый вечер\nУ всех сегодня билайн домашний инет не работал?\n\nА теперь всё с ограничениями?",
+ "date": "2026-03-25T15:47:14.973788",
+ "topic": null
+ },
+ {
+ "id": 77,
+ "user_id": 864147545,
+ "text": "У718АС 73 хендай элантра не нужен о заезжать на «газон»(в нашем случае грязь)",
+ "date": "2026-03-25T16:59:42.057708",
+ "topic": null
+ },
+ {
+ "id": 78,
+ "user_id": 864147545,
+ "text": null,
+ "date": "2026-03-25T17:59:17.637774",
+ "topic": null
+ },
+ {
+ "id": 79,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-03-26T08:25:52.691712",
+ "topic": null
+ },
+ {
+ "id": 80,
+ "user_id": 1667052403,
+ "text": "Да да\nИнета нет\nА платим исправно....",
+ "date": "2026-03-26T08:32:56.795527",
+ "topic": null
+ },
+ {
+ "id": 81,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-03-27T15:01:39.815229",
+ "topic": null
+ },
+ {
+ "id": 82,
+ "user_id": 5036016769,
+ "text": null,
+ "date": "2026-03-27T16:54:15.376935",
+ "topic": null
+ },
+ {
+ "id": 83,
+ "user_id": 5036016769,
+ "text": "Где же вы все были когда снег там лежал",
+ "date": "2026-03-27T16:54:45.860697",
+ "topic": null
+ },
+ {
+ "id": 84,
+ "user_id": 1476737008,
+ "text": "Добрый вечер.Соседи,подскажите пожалуйста где сделать ключ от домофона",
+ "date": "2026-03-28T17:29:19.880276",
+ "topic": "соседи"
+ },
+ {
+ "id": 85,
+ "user_id": 368569899,
+ "text": "Добрый вечер. Мы пару раз делали в Лидере на 1 этаже, там где ключи делают.",
+ "date": "2026-03-28T17:37:29.003831",
+ "topic": "домофон"
+ },
+ {
+ "id": 86,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-03-29T06:10:46.673382",
+ "topic": null
+ },
+ {
+ "id": 87,
+ "user_id": 1670557872,
+ "text": ")))",
+ "date": "2026-03-29T06:12:19.450560",
+ "topic": null
+ },
+ {
+ "id": 88,
+ "user_id": 1476737008,
+ "text": "Спасибо большое)",
+ "date": "2026-03-29T06:46:04.110315",
+ "topic": "благодарности"
+ },
+ {
+ "id": 89,
+ "user_id": 1667052403,
+ "text": null,
+ "date": "2026-03-29T18:03:20.251284",
+ "topic": null
+ },
+ {
+ "id": 90,
+ "user_id": 1667052403,
+ "text": "https://ulpressa.ru/2026/03/28/upravdom-provajdery-v-mkd-2-dlya-kogo-zakony-pisany/",
+ "date": "2026-03-29T18:21:13.686290",
+ "topic": null
+ },
+ {
+ "id": 91,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-05T11:51:00.860105",
+ "topic": null
+ },
+ {
+ "id": 92,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-05T11:51:00.877190",
+ "topic": null
+ },
+ {
+ "id": 93,
+ "user_id": 1667052403,
+ "text": "К старшей и в ук \nНаверное \nСейчас концов не найдешь",
+ "date": "2026-04-05T11:51:00.903741",
+ "topic": "ук"
+ },
+ {
+ "id": 94,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-05T13:38:06.074054",
+ "topic": null
+ },
+ {
+ "id": 95,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-05T13:38:06.092502",
+ "topic": null
+ },
+ {
+ "id": 96,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-05T13:38:06.107622",
+ "topic": null
+ },
+ {
+ "id": 97,
+ "user_id": 197957361,
+ "text": "А как дела обстоят в ваших подъездах? \nЕсть такие \"дремучие\", которые никак не поймут, что с мусором делать? \n\nМожет у кого то из окна пакеты выбрасывают ? 😁",
+ "date": "2026-04-05T13:38:59.369432",
+ "topic": "уборка"
+ },
+ {
+ "id": 98,
+ "user_id": 576801733,
+ "text": "Искренне верим что завтра вывезут... я так и не поняла зачем они сложили мешки и пропали.... с кры я имею ввиду",
+ "date": "2026-04-05T13:44:27.580631",
+ "topic": null
+ },
+ {
+ "id": 99,
+ "user_id": 576801733,
+ "text": "Крыши... т9 зараза",
+ "date": "2026-04-05T13:44:46.797929",
+ "topic": null
+ },
+ {
+ "id": 100,
+ "user_id": 772234481,
+ "text": "Не всем легко эти большие баулы с мусором относить за дом,потому что они тяжелые.Я не понимаю постоянные претензии по поводу этого.(К мешкам я не причастна,но эта тема из раза в раз оговаривается в чате,зачем-не знаю,мне кажется и так все всё видят и понимают,не могу сказать точно когда,но раз в неделю приезжает машина и забирает такой мусор,можно к этому и по спокойнее относиться)\nПусть тогда все салоны/магазины/пвз и тд со стороны проспекта,куда нужно относить мусор по вашему, закроют и уберут.Потому что они закрывают второй запасной выход из подъезда.Если бы вторые запасные выходы из подъездов не были бы напичканы салонами,то мусор можно было спокойно выносить туда,на ту сторону к бакам-было бы легче и правильнее.",
+ "date": "2026-04-05T14:01:57.183265",
+ "topic": "уборка"
+ },
+ {
+ "id": 101,
+ "user_id": 197957361,
+ "text": "у нас внутри двора рекреации под это имеются\nи это отродясь не вызывало проблем никому и никогда \n\nно вот с появлением новых жильцов такое бывает, например у 5го подьезда это тупо бытовой мусор с кухни! \nнадо быть очень диким и далеким от цивилизации, чтобы выбрасывать такой мусор - под дверь подьезда",
+ "date": "2026-04-05T14:34:07.586221",
+ "topic": "уборка"
+ },
+ {
+ "id": 102,
+ "user_id": 772234481,
+ "text": "Про бытовой мусор с кухни согласна,это дикость",
+ "date": "2026-04-05T14:46:27.710436",
+ "topic": "уборка"
+ },
+ {
+ "id": 103,
+ "user_id": 772234481,
+ "text": "У 7,8,9 подъезда есть места(выход к бакам)куда можно выкидывать крупногабаритный мусор,туда уборочные машины почему-то приезжают чаще и забирают,но опять таки,когда жильцы относили мусор туда,другие жильцы из этих подъездов ругались, и высказывали своё негодование,мол зачем весь мусор относят к их подъезду",
+ "date": "2026-04-05T14:53:26.450162",
+ "topic": "уборка"
+ },
+ {
+ "id": 104,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-05T15:05:16.857463",
+ "topic": null
+ },
+ {
+ "id": 105,
+ "user_id": 1335945859,
+ "text": "Если бы увозили с такой периодичностью, чтобы не образовывалась СВАЛКА у подъезда, никто бы, я думаю, не возмущался",
+ "date": "2026-04-05T15:24:23.779266",
+ "topic": "уборка"
+ },
+ {
+ "id": 106,
+ "user_id": 1667052403,
+ "text": "Это же и про подъезды можно сказать... Увозили бы раз в неделю и разговоров бы не было\n\nКак и с уборкой подъездов и двора.\nФормулировка - по необходимости - приводит к грязи..и хаосу...",
+ "date": "2026-04-05T15:26:34.820124",
+ "topic": "уборка"
+ },
+ {
+ "id": 107,
+ "user_id": 1335945859,
+ "text": "Потому что тот кто определяет эту необходимость делает это так как считает нужным именно он (она)",
+ "date": "2026-04-05T15:31:32.428514",
+ "topic": null
+ },
+ {
+ "id": 108,
+ "user_id": 197957361,
+ "text": "А у нас разве вывоз такого мусора - никак не регламентирован ? \nВот просто не верю, по простой причине - оплата то у нас за это идет не абы как, а регулярная и постоянная.\nТак что и график там должен быть регулярный и постоянный",
+ "date": "2026-04-05T15:33:08.750641",
+ "topic": "мусор"
+ },
+ {
+ "id": 109,
+ "user_id": 576801733,
+ "text": "Уважаемые соседи, можно вопрос не про мусор, видео кто нибудь оплачивает? У меня в этом году какой то просто адский тариф и за 2 месяца 900 с лишним рублей((( подскажите плиз тариф у нас какой? А Домофон у кого нибудь вообще работает не с ключа?",
+ "date": "2026-04-05T15:33:40.638091",
+ "topic": "соседи"
+ },
+ {
+ "id": 110,
+ "user_id": 197957361,
+ "text": "Или тоже, как с камерами. Все не при чем, молиться в сторону ульяновского 2, и только по троекратному письменному разрешению неизвестно кого ? 🤣",
+ "date": "2026-04-05T15:33:54.688085",
+ "topic": null
+ },
+ {
+ "id": 111,
+ "user_id": 1667052403,
+ "text": "💯",
+ "date": "2026-04-05T15:40:06.767521",
+ "topic": null
+ },
+ {
+ "id": 112,
+ "user_id": 197957361,
+ "text": "а вот и первое сообщение о платежке в почтовом ящике - и там тариф, который не мы с вами НЕ ВЫБИРАЛИ",
+ "date": "2026-04-05T16:13:55.637380",
+ "topic": null
+ },
+ {
+ "id": 113,
+ "user_id": 197957361,
+ "text": "🔥Для жильцов нашего дома поясню\nНа прошлой неделе, благодаря инициативной группе, я побывал в отделении ОБЭП\nКоторое начало расследовать \"А что тут происходит в доме вообще?\"\nВсе эти таинственные пропадающие договора, все эти документы которых нигде нет, вся эта кутерьма с видеокамерами, вся эта ерунда с оплатой домофона самовольной по метражу, по обрезкам, по ремонтам, по уборке тракторами и метлами. \n\nУ нас за года СТОЛЬКО претензий к УО ЖСС накопилось!\n\nИ вот как то на встречу они совершенно не идут, не хотят разобраться ни в одном вопросе. Просто - в молчанку идут. Считают, что все норм, что все везде на тормозах сойдет. \n\nИ потому, нам нужна лишь платежка, как доказательства очередного их \"выкрутаса\", который совершенно не совместим ни с жилищным законодательством, ни со здравым смыслом! Взяли и сами себе тариф придумали ! Вот это выкрутасы ! \nЭто, чтобы все жильцы понимали, ну в первую очередь их зарплата например. \n\n🫡Кто нибудь может сам себе зарплату поднять на нужную ему сумму ? Просто так. В одно лицо. \nНет ? А почему интересно ? \n\nВот и мы, собственники в доме - РАБОТАДАТЕЛЬ для УО ЖСС. И сотрудник наш - почему то решил, что может сам себе зарплату назначать. \nХотелки - их, а карман то - наш. \n\nКому то это кажется правильным ? \nДля справки, со стороны закона - это нонсенс! \n\nПотому с этими их выходками конечно надо закончить. Это наглость какая то несусветная. Можно и покрепче выразиться.",
+ "date": "2026-04-05T16:13:58.099823",
+ "topic": "собрание"
+ },
+ {
+ "id": 114,
+ "user_id": 1667052403,
+ "text": "🔥🔥🔥",
+ "date": "2026-04-05T16:32:19.532647",
+ "topic": null
+ },
+ {
+ "id": 115,
+ "user_id": 1179832004,
+ "text": "Доброе утро,9 подъезд 3-ка нет воды,что случилось?",
+ "date": "2026-04-06T05:21:23.714970",
+ "topic": "уборка"
+ },
+ {
+ "id": 116,
+ "user_id": 5235580033,
+ "text": "Щенка никто не терял 6 подъезд, предположительно хаски",
+ "date": "2026-04-07T15:59:12.269686",
+ "topic": "уборка"
+ },
+ {
+ "id": 117,
+ "user_id": 5246728667,
+ "text": "Здравствуйте, а в чем проблема вторую ночь подряд нет напора холодной воды? Днем всё хорошо, а ночью холодная вода еле еле течёт.",
+ "date": "2026-04-07T21:13:03.405086",
+ "topic": "вода"
+ },
+ {
+ "id": 118,
+ "user_id": 458413170,
+ "text": "Добрый день. Пришел счёт в приложение банка. Скажите, это что вообще",
+ "date": "2026-04-09T14:09:47.993980",
+ "topic": null
+ },
+ {
+ "id": 119,
+ "user_id": 458413170,
+ "text": null,
+ "date": "2026-04-09T14:10:04.260985",
+ "topic": null
+ },
+ {
+ "id": 120,
+ "user_id": 458413170,
+ "text": null,
+ "date": "2026-04-09T14:10:04.284659",
+ "topic": null
+ },
+ {
+ "id": 121,
+ "user_id": 462572897,
+ "text": "Возможно это капремонт. За несколько месяцев, если не платили ...",
+ "date": "2026-04-09T14:14:34.659087",
+ "topic": "соседи"
+ },
+ {
+ "id": 122,
+ "user_id": 458413170,
+ "text": "Так за капремонт разве мы не оплачиваем ежемесячно в РИЦ?",
+ "date": "2026-04-09T14:20:25.906081",
+ "topic": "соседи"
+ },
+ {
+ "id": 123,
+ "user_id": 458413170,
+ "text": "Я так понимаю, оплачиваем все",
+ "date": "2026-04-09T14:55:28.518224",
+ "topic": null
+ },
+ {
+ "id": 124,
+ "user_id": 458413170,
+ "text": null,
+ "date": "2026-04-09T14:55:33.063509",
+ "topic": null
+ },
+ {
+ "id": 125,
+ "user_id": 8181748817,
+ "text": "В этой платежке указано что платеж за кап.ремонт за март 26г. (при цене 15,05 руб за кв.м и площади квартиры 66,8 м.кв). \nОплата проводится ежемесячно через РИЦ на спецсчет дома.\nВсе, больше ничего по кап.ремонту платить не нужно.\n\nНикаких прямых платежей в фонд кап.ремонта не должно быть. Счет на 4820 руб - возможно от мошеников, будьте аккуратнее, никаких действий не производите. \n\nОбратитесь в банк, от кого выставлен счет, там выявят, если от мошеников.",
+ "date": "2026-04-09T19:27:30.222466",
+ "topic": "соседи"
+ },
+ {
+ "id": 126,
+ "user_id": 8181748817,
+ "text": "В этой платежке указано что платеж за кап.ремонт за март 26г. (при цене 15,05 руб за кв.м и площади квартиры 66,8 м.кв). \nОплата проводится ежемесячно через РИЦ на спецсчет дома.\nВсе, больше ничего по кап.ремонту платить не нужно.\n\nНикаких прямых платежей в фонд кап.ремонта не должно быть. Счет на 4820 руб - возможно от мошеников, будьте аккуратнее, никаких действий не производите. \n\nОбратитесь в банк, от кого выставлен счет, там выявят, если от мошеников.",
+ "date": "2026-04-09T19:32:58.749051",
+ "topic": "соседи"
+ },
+ {
+ "id": 127,
+ "user_id": 1335945859,
+ "text": "Раньше я боялся темноты, но вчера принесли квитанции. Теперь я боюсь света, воды и тепла. И еще немного — мусора.",
+ "date": "2026-04-10T15:16:15.029712",
+ "topic": "уборка"
+ },
+ {
+ "id": 128,
+ "user_id": 7346013939,
+ "text": "Добрый день! Как наши успехи, деньги собрали на пошлину, теперь проблема с телегой. Информации мало.",
+ "date": "2026-04-11T08:03:52.949041",
+ "topic": null
+ },
+ {
+ "id": 129,
+ "user_id": 1353946650,
+ "text": null,
+ "date": "2026-04-11T12:03:02.956710",
+ "topic": null
+ },
+ {
+ "id": 130,
+ "user_id": 5183810334,
+ "text": null,
+ "date": "2026-04-12T04:35:40.077555",
+ "topic": null
+ },
+ {
+ "id": 131,
+ "user_id": 2139751123,
+ "text": "ВО ИСТИНУ ВОСКРЕСЕ!!!\nХРИСТОС ВОСКРЕСЕ!!!\nХРИСТОС ВОСКРЕСЕ!!!\nХРИСТОС ВОСКРЕСЕ!!!",
+ "date": "2026-04-12T06:32:17.398539",
+ "topic": null
+ },
+ {
+ "id": 132,
+ "user_id": 1667052403,
+ "text": null,
+ "date": "2026-04-12T06:49:15.228600",
+ "topic": null
+ },
+ {
+ "id": 133,
+ "user_id": 5183810334,
+ "text": null,
+ "date": "2026-04-12T06:57:10.820177",
+ "topic": null
+ },
+ {
+ "id": 134,
+ "user_id": 8181748817,
+ "text": "🙏🙏🌞💐",
+ "date": "2026-04-12T07:16:38.356136",
+ "topic": null
+ },
+ {
+ "id": 135,
+ "user_id": 8181748817,
+ "text": "🙏🙏🌞💐",
+ "date": "2026-04-12T07:16:38.400835",
+ "topic": null
+ },
+ {
+ "id": 136,
+ "user_id": 576801733,
+ "text": "ВОИСТИНУ ВОСКРЕСЕ!!!!!🙏🙏💐🌷🌹",
+ "date": "2026-04-12T07:41:00.515836",
+ "topic": null
+ },
+ {
+ "id": 137,
+ "user_id": 1670557872,
+ "text": null,
+ "date": "2026-04-12T08:04:05.021389",
+ "topic": null
+ },
+ {
+ "id": 138,
+ "user_id": 5106504485,
+ "text": null,
+ "date": "2026-04-12T10:36:28.845060",
+ "topic": null
+ },
+ {
+ "id": 139,
+ "user_id": 5106504485,
+ "text": null,
+ "date": "2026-04-12T10:36:50.775138",
+ "topic": null
+ },
+ {
+ "id": 140,
+ "user_id": 5106504485,
+ "text": "1подъезд🫣",
+ "date": "2026-04-12T10:36:53.349640",
+ "topic": "уборка"
+ },
+ {
+ "id": 141,
+ "user_id": 1970561536,
+ "text": "Детки голодные,ели сразу",
+ "date": "2026-04-12T10:38:08.866439",
+ "topic": null
+ },
+ {
+ "id": 142,
+ "user_id": 5106504485,
+ "text": "Это они не ели,а раскидали на три этажа",
+ "date": "2026-04-12T10:39:51.117025",
+ "topic": null
+ },
+ {
+ "id": 143,
+ "user_id": 462572897,
+ "text": "Не воспитанные, родители взрастили вседозволенность. Хоть праздник, хоть что ... Дикость.",
+ "date": "2026-04-12T14:05:15.606018",
+ "topic": null
+ },
+ {
+ "id": 144,
+ "user_id": 197957361,
+ "text": "Не пойму никак, яйца то при чем - вроде день Гагарина сегодня ? не ? \nВпрочем это лирика ! \nА что у нас по гамбургскому счету ?",
+ "date": "2026-04-12T14:13:52.405574",
+ "topic": null
+ },
+ {
+ "id": 145,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-12T14:14:15.163391",
+ "topic": null
+ },
+ {
+ "id": 146,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-12T14:14:26.193642",
+ "topic": null
+ },
+ {
+ "id": 147,
+ "user_id": 197957361,
+ "text": "📌 Мы тут с инициативными жильцами обсуждали буквально на днях, что пора бы подавать в суд, иском прям, на нашу \"любимую\" управляющую организацию УО ЖСС, и ее \"опытного бухгалтера\" по совместительству председателя текущего нашего дома - гражданку Ерееву Т.М. \nИ знаете, таки иск в суд отправлен ! \nНу а раз формальные правила требуют от нас отправки копии иска - в адрес Ответчика (УО ЖСС), то им оно ушло первым делом. \n\nТеперь в понедельник и УО ЖСС, и суд заволжский, получат иски\nГде черным по белому будет написано кое что интересное из жизни нашей УО ЖСС в плане \"обмишуривания\" \"обдуривания\" нашего дома в плане расчетов и тарифов, и всех событий первых трех месяцев 2026 года. \n\nОгромное спасибо нашим юристам, огромное спасибо нашим экономистам, огромное спасибо всем кто поучаствовал в сборе 💰 на госпошлину для суда. Все собралось, все написалось, все факты и замечания учтены, иски ушли и теперь нам осталось лишь подождать что нам суд на это ответит. \n\nps я не юрист, но прочитав все, мне самому стало интересно, можно ли используя юридические инсинуации и всякие там \"я с Петровичем на рыбалку езжу\" умудриться в суде нормами юридическими действующего законодательства победить законы физики и геометрии нашей вселенной?? В силу образования направленного на ✈️авиастроительство - не вижу ни единого такого выхода.",
+ "date": "2026-04-12T14:20:35.418521",
+ "topic": "ук"
+ },
+ {
+ "id": 148,
+ "user_id": 1667052403,
+ "text": "Спасибо Александр",
+ "date": "2026-04-12T15:12:27.802314",
+ "topic": "благодарности"
+ },
+ {
+ "id": 149,
+ "user_id": 197957361,
+ "text": "Погоды не позволяют пока что беседы неспешные во дворе проводить \nНо вот за неделю с несколькими жильцами я тут повстречался, минут по сорок-часу побеседовали\n\nГлаза квадратные у людей от происходящего, шашками хотят махать, писать и прочее\n\nВот уж где инициативная группа поработает весною теплою ! Вот где люди прозреют",
+ "date": "2026-04-12T15:14:13.060243",
+ "topic": "вода"
+ },
+ {
+ "id": 150,
+ "user_id": 259925158,
+ "text": null,
+ "date": "2026-04-12T17:50:38.219022",
+ "topic": null
+ },
+ {
+ "id": 151,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-13T08:35:21.162041",
+ "topic": null
+ },
+ {
+ "id": 152,
+ "user_id": 576801733,
+ "text": "Так нам отказали? Что то я запуталась",
+ "date": "2026-04-13T18:41:44.278998",
+ "topic": null
+ },
+ {
+ "id": 153,
+ "user_id": 5246728667,
+ "text": "Здравствуйте, а почему иск подан только от одного жителя, а не от всех? Это было бы не эффективней?",
+ "date": "2026-04-13T18:57:42.401189",
+ "topic": null
+ },
+ {
+ "id": 154,
+ "user_id": 5246728667,
+ "text": "Рассмотреть дело в его отсутствии, а доказательная база там какая-то приложена? Фото раслейки объявлений, скрины и тд..",
+ "date": "2026-04-13T19:02:18.819016",
+ "topic": "объявления"
+ },
+ {
+ "id": 155,
+ "user_id": 197957361,
+ "text": "Было бы конечно\nТолько тогда каждый кто хотел бы там присутствовать - должен бы был сделать нотариальную доверенность на того, кто подает сам иск, ну и составить список-реестр всех таких желающих. \n\nОдин иск, в котором было написано что он коллективный - недавно по формальным признакам - суд спокойно развернул. Оказывается коли он коллективный - надо чтобы его подавал человек с дипломом юриста. \nУ нас в доме есть дипломированные юристы, но что то не горят желанием отправлять такие иски. \nНу и снова, нотариальные доверенности от каждого жителя - это поход каждого желающего к адвокату, и сумма денег за заверение. \n\nТак что выбор то есть, но видишь, какой он странный этот выбор. \n\nНо ничего страшного, юристы же все пояснили. Само решение суда - будет одинаковое, что иск от одного человека, что от двести пятьдесят восьми.",
+ "date": "2026-04-13T19:03:16.995010",
+ "topic": null
+ },
+ {
+ "id": 156,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-13T19:04:15.880027",
+ "topic": null
+ },
+ {
+ "id": 157,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-13T19:04:15.899573",
+ "topic": null
+ },
+ {
+ "id": 158,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-13T19:04:15.914759",
+ "topic": null
+ },
+ {
+ "id": 159,
+ "user_id": 197957361,
+ "text": "База у нас там чуть ли не на полкилограмма\nСуду что надо будет еще - запросят, без проблем предоставим\n\nПущай вон ОБЭП запрашивают, следователь кстати сразу сказал, что когда иск пойдет - так и написать, он все предоставит",
+ "date": "2026-04-13T19:05:05.470710",
+ "topic": "уборка"
+ },
+ {
+ "id": 160,
+ "user_id": 5246728667,
+ "text": "Надеюсь всё получится 💪",
+ "date": "2026-04-13T19:16:41.766248",
+ "topic": "уборка"
+ },
+ {
+ "id": 161,
+ "user_id": 197957361,
+ "text": "Конечно получится !",
+ "date": "2026-04-13T19:17:22.675385",
+ "topic": "уборка"
+ },
+ {
+ "id": 162,
+ "user_id": 1670557872,
+ "text": "Вот вообще не удивлюсь, что каким-то образом суд тоже не увидит правонарушений.... И скажет что ЖСС наш молодец. Что-то куда Саша не обращается, везде абсолютно отписки",
+ "date": "2026-04-14T04:16:50.938317",
+ "topic": "благодарности"
+ },
+ {
+ "id": 163,
+ "user_id": 1335945859,
+ "text": "Надо верить в лучшее) все будет хорошо)",
+ "date": "2026-04-14T04:29:58.430410",
+ "topic": "благодарности"
+ },
+ {
+ "id": 164,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-14T06:25:17.741926",
+ "topic": null
+ },
+ {
+ "id": 165,
+ "user_id": 197957361,
+ "text": "https://www.rupor73.ru/obshchestvo/45208-karlinu-postavili-neud-za-rabotu-gosstrojzhilnadzora",
+ "date": "2026-04-15T02:10:12.122316",
+ "topic": null
+ },
+ {
+ "id": 166,
+ "user_id": 1667052403,
+ "text": "⚡️ Российские власти могут повременить с ужесточением контроля над интернетом, особенно в отношении Telegram \n\nПричиной стали опасения, что это сказывается на рейтинге в связи с выборами, сообщили источники Bloomberg.\n\n😱 Подписаться | Прислать новость",
+ "date": "2026-04-15T04:15:28.556000",
+ "topic": null
+ },
+ {
+ "id": 167,
+ "user_id": 1670557872,
+ "text": "Только в связи с выборами!)",
+ "date": "2026-04-15T04:48:02.097930",
+ "topic": null
+ },
+ {
+ "id": 168,
+ "user_id": 2139751123,
+ "text": "\"Вор должен сидеть в тюрьме...\", но мафия бессмертна 🤔",
+ "date": "2026-04-15T06:18:46.540146",
+ "topic": null
+ },
+ {
+ "id": 169,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-17T04:14:14.598897",
+ "topic": null
+ },
+ {
+ "id": 170,
+ "user_id": 197957361,
+ "text": null,
+ "date": "2026-04-17T13:11:10.664133",
+ "topic": null
+ }
+ ]
+}
\ No newline at end of file
diff --git a/main.py b/main.py
index 69e2537..3bbb463 100644
--- a/main.py
+++ b/main.py
@@ -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(
diff --git a/services/analytics.py b/services/analytics.py
index 6bf05a1..95140b1 100644
--- a/services/analytics.py
+++ b/services/analytics.py
@@ -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 'нет данных'
-
- 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)
+ if not rows: return 'нет данных'
+ return ', '.join([f'{i}. {r[1] or f"user{r[0]}"}: {r[2]}' for i, r in enumerate(rows, 1)])
- 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())
- # Получаем сообщения за последнюю неделю
- 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 = select(Message).order_by(Message.timestamp.asc())
+ if not full_history:
+ week_ago = datetime.utcnow() - timedelta(days=7)
+ 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,
- }
diff --git a/services/analytics_reference.py b/services/analytics_reference.py
new file mode 100644
index 0000000..6bf05a1
--- /dev/null
+++ b/services/analytics_reference.py
@@ -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,
+ }
diff --git a/services/chat_exporter.py b/services/chat_exporter.py
new file mode 100644
index 0000000..7426c4d
--- /dev/null
+++ b/services/chat_exporter.py
@@ -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}
diff --git a/services/scheduler.py b/services/scheduler.py
index 096aa63..6ed8db0 100644
--- a/services/scheduler.py
+++ b/services/scheduler.py
@@ -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'📥 Еженедельный экспорт выполнен\n\n'
- f'Файл сохранён: {filepath}\n'
- f'Время: {datetime.utcnow().strftime("%d.%m.%Y %H:%M")}'
+ f'📥 Еженедельный экспорт завершён\n\n'
+ f'📄 Файл: {result.get("filename")}\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'❌ Ошибка экспорта\n\n{str(e)}'
- )
+ await self.bot.send_message(ADMIN_USER_ID, f'❌ Ошибка экспорта\n\n{str(e)}')
async def daily_spy_check(self):
"""Ежедневная проверка на шпионов"""
diff --git a/test_web.py b/test_web.py
new file mode 100644
index 0000000..4b2d8b6
--- /dev/null
+++ b/test_web.py
@@ -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)
diff --git a/web/app.py b/web/app.py
index 22951c0..fddd9fd 100644
--- a/web/app.py
+++ b/web/app.py
@@ -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"
-
- 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}/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})
+
+@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("/schedules", response_class=HTMLResponse)
+async def schedules_page(request: Request, username: str = Depends(get_current_admin)):
+ async with AsyncSessionLocal() as session:
+ 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})
-@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})
+# ============================================================================
+# РАССЫЛКИ (SMART BROADCAST)
+# ============================================================================
-@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:
- async with AsyncSessionLocal() as session:
- # Получаем ВСЕХ пользователей
- users_res = await session.execute(select(User))
- users = users_res.scalars().all()
-
- # Получаем ВСЕ сообщения (без лимита 1000)
- messages_res = await session.execute(select(Message).order_by(Message.timestamp.asc()))
- messages = messages_res.scalars().all()
-
- 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
- ]
- }
-
- 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("/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})
+
+# ============================================================================
+# ЗАПЛАНИРОВАННЫЕ ПОСТЫ
+# ============================================================================
@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)
diff --git a/web/app_reference.py b/web/app_reference.py
new file mode 100644
index 0000000..f30c27a
--- /dev/null
+++ b/web/app_reference.py
@@ -0,0 +1,1547 @@
+"""
+Web Admin Interface v3.0 - MEGA ADMIN PANEL
+Полноценное управление ВСЕМ функционалом бота
+"""
+import logging
+import json
+import aiohttp
+from pathlib import Path
+from fastapi import FastAPI, Request, Depends, HTTPException, status, Form, File, UploadFile
+from fastapi.security import HTTPBasic, HTTPBasicCredentials
+from fastapi.staticfiles import StaticFiles
+from fastapi.templating import Jinja2Templates
+from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
+from fastapi.middleware.cors import CORSMiddleware
+from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy import select, func, update, delete, desc
+from datetime import datetime, timedelta, timezone
+import secrets
+import config
+
+# ===== ЧАСОВОЙ ПОЯС (Ульяновск, UTC+4) =====
+LOCAL_TZ = timezone(timedelta(hours=4)) # Самара/Ульяновск
+
+def to_local(dt: datetime) -> datetime:
+ """Конвертировать UTC datetime в локальное время (Ульяновск, UTC+4)"""
+ if dt is None:
+ return None
+ if dt.tzinfo is None:
+ # Считаем что naive datetime — это UTC
+ dt = dt.replace(tzinfo=timezone.utc)
+ return dt.astimezone(LOCAL_TZ)
+
+def fmt_local(dt: datetime, fmt: str = '%d.%m.%Y %H:%M') -> str:
+ """Форматировать UTC datetime в строку локального времени"""
+ 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
+)
+
+logger = logging.getLogger(__name__)
+
+app = FastAPI(title="Домовой Бот - MEGA Admin Panel", version="3.0.0")
+
+# Добавляем CORS middleware
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"], # Разрешаем все origins (для локального использования)
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+templates = Jinja2Templates(directory="web/templates")
+app.mount("/static", StaticFiles(directory="web/static"), name="static")
+app.mount("/static/service_images", StaticFiles(directory="data/service_images"), name="service_images")
+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"})
+ return credentials.username
+
+
+# ============================================================================
+# ГЛАВНАЯ
+# ============================================================================
+
+@app.get("/", response_class=HTMLResponse)
+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()
+ 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()
+
+ # Получаем количество ожидающих верификации
+ from database.models import VerificationRequest
+ pending_count = (await session.execute(
+ select(func.count(VerificationRequest.id)).where(VerificationRequest.status == 'pending')
+ )).scalar()
+
+ 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
+ })
+
+
+# ============================================================================
+# ПОЛЬЗОВАТЕЛИ
+# ============================================================================
+
+@app.get("/users", response_class=HTMLResponse)
+async def users_page(request: Request, search: str = "", filter: str = "", username: str = Depends(get_current_admin)):
+ """Пользователи с фильтрами"""
+ async with AsyncSessionLocal() as session:
+ 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':
+ # Только ИГ
+ from database.models import InitiativeGroup
+ ig_stmt = select(InitiativeGroup.user_id).where(InitiativeGroup.is_active == True)
+ ig_result = await session.execute(ig_stmt)
+ ig_user_ids = [row[0] for row in ig_result.all()]
+ stmt = stmt.where(User.user_id.in_(ig_user_ids))
+ # filter == 'all' или пустой — показываем всех
+
+ # Поиск
+ if 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}%"))
+ )
+
+ stmt = stmt.order_by(User.user_id.desc()).limit(100)
+ users = list((await session.execute(stmt)).scalars().all())
+
+ # Получаем список ИГ
+ from database.models import InitiativeGroup
+ ig_stmt = select(InitiativeGroup.user_id).where(InitiativeGroup.is_active == True)
+ ig_result = await session.execute(ig_stmt)
+ ig_user_ids = set(row[0] for row in ig_result.all())
+
+ # Получаем заявки на верификацию
+ from database.models import VerificationRequest
+ pending_stmt = select(VerificationRequest).where(
+ VerificationRequest.status == 'pending'
+ ).order_by(VerificationRequest.created_at.desc())
+ pending_result = await session.execute(pending_stmt)
+ pending_requests = [
+ {
+ 'id': req.id,
+ 'user_id': req.user_id,
+ 'user_name': req.user.full_name if req.user else None,
+ 'username': req.user.username if req.user else None,
+ 'apartment': req.apartment,
+ 'created_at': req.created_at
+ }
+ for req in pending_result.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()
+ unverified_count = total_count - verified_count
+ active_unverified = (await session.execute(
+ select(func.count(User.user_id)).where(User.verified == False, User.message_count >= 50)
+ )).scalar()
+ ig_count = len(ig_user_ids)
+
+ return templates.TemplateResponse("users.html",
+ {"request": request, "username": username, "users": users, "search": search, "current_filter": filter,
+ "stats": {"total": total_count, "verified": verified_count, "unverified": unverified_count, "active_unverified": active_unverified, "ig": ig_count},
+ "ig_user_ids": ig_user_ids,
+ "pending_requests": pending_requests,
+ "pending_count": len(pending_requests)})
+
+
+@app.post("/api/users/auto_verify")
+async def api_auto_verify_users(username: str = Depends(get_current_admin)):
+ """Автоверификация активных пользователей (ТОЛЬКО с квартирой!)"""
+ async with AsyncSessionLocal() as session:
+ # Находим активных (50+ сообщений) которые не верифицированы
+ # И у которых УЖЕ ЕСТЬ квартира!
+ stmt = select(User).where(
+ User.verified == False,
+ User.message_count >= 50,
+ User.apartment != None,
+ User.apartment != ''
+ )
+ result = await session.execute(stmt)
+ active_users = list(result.scalars().all())
+
+ # Верифицируем всех
+ verified_count = 0
+ for user in active_users:
+ user.verified = True
+ if not user.verification_date:
+ user.verification_date = datetime.utcnow()
+ verified_count += 1
+
+ await session.commit()
+
+ return JSONResponse({"success": True, "message": f"Верифицировано {verified_count} активных пользователей с квартирой"})
+
+
+@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:
+ 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 'banned' in data: update_data['is_banned'] = bool(data['banned'])
+ 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.get("/api/user/{user_id}")
+async def api_get_user(user_id: int, username: str = Depends(get_current_admin)):
+ """Получить данные пользователя"""
+ async with AsyncSessionLocal() as session:
+ user = await session.get(User, user_id)
+
+ if not user:
+ return JSONResponse({"error": "Пользователь не найден"}, 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/user/{user_id}/ban")
+async def api_ban_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(is_banned=True))
+ await session.commit()
+ return JSONResponse({"success": True, "message": "Забанен"})
+
+
+@app.post("/api/user/{user_id}/unban")
+async def api_unban_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(is_banned=False))
+ await session.commit()
+ return JSONResponse({"success": True, "message": "Разбанен"})
+
+
+@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, "message": "Верифицирован"})
+
+
+@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, "message": "Верификация отозвана"})
+
+
+@app.post("/api/user/{user_id}/make_admin")
+async def api_make_admin(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(is_admin=True))
+ await session.commit()
+ return JSONResponse({"success": True, "message": "Пользователь теперь админ"})
+
+
+@app.post("/api/user/{user_id}/remove_admin")
+async def api_remove_admin(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(is_admin=False))
+ await session.commit()
+ return JSONResponse({"success": True, "message": "Админ снят"})
+
+
+@app.post("/api/user/{user_id}/add_to_ig")
+async def api_add_to_ig(user_id: int, username: str = Depends(get_current_admin)):
+ """Добавить в Инициативную Группу"""
+ from services.initiative_group import InitiativeGroupService
+
+ async with AsyncSessionLocal() as session:
+ ig_service = InitiativeGroupService(session)
+ success = await ig_service.add_member(user_id, 'member')
+
+ if success:
+ return JSONResponse({"success": True, "message": "Добавлен в ИГ"})
+ else:
+ return JSONResponse({"error": "Не удалось добавить в ИГ"}, status_code=400)
+
+
+@app.post("/api/user/{user_id}/remove_from_ig")
+async def api_remove_from_ig(user_id: int, username: str = Depends(get_current_admin)):
+ """Удалить из Инициативной Группы"""
+ from services.initiative_group import InitiativeGroupService
+
+ async with AsyncSessionLocal() as session:
+ ig_service = InitiativeGroupService(session)
+ success = await ig_service.remove_member(user_id)
+
+ if success:
+ return JSONResponse({"success": True, "message": "Удалён из ИГ"})
+ else:
+ return JSONResponse({"error": "Не найден в ИГ"}, status_code=400)
+
+
+# ============================================================================
+# ВЕРИФИКАЦИЯ
+# ============================================================================
+
+@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')
+ .order_by(VerificationRequest.created_at.desc()).limit(50)
+ )).scalars().all())
+ unverified = list((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("phones.html", {
+ "request": request, "username": username, "services": services
+ })
+
+
+@app.post("/api/service/add")
+async def api_add_service(request: Request, username: str = Depends(get_current_admin)):
+ """Добавить службу"""
+ data = await request.json()
+ async with AsyncSessionLocal() as session:
+ service = Service(
+ name=data['name'],
+ phone=data['phone'],
+ category=data.get('category', 'other'),
+ description=data.get('description', ''),
+ verified_by_admin=True,
+ image_path=data.get('image_path', None)
+ )
+ session.add(service)
+ await session.commit()
+ return JSONResponse({"success": True, "message": "Служба добавлена"})
+
+
+@app.post("/api/service/upload_image")
+async def api_upload_service_image(
+ file: UploadFile = File(...),
+ category: str = Form(...),
+ username: str = Depends(get_current_admin)
+):
+ """Загрузить картинку для службы"""
+ import os
+ import uuid
+ from pathlib import Path
+
+ # Создаём папку для картинок
+ images_dir = Path("data/service_images")
+ images_dir.mkdir(parents=True, exist_ok=True)
+
+ # Генерируем уникальное имя файла
+ file_extension = file.filename.split(".")[-1] if "." in file.filename else "jpg"
+ unique_filename = f"{category}_{uuid.uuid4().hex}.{file_extension}"
+ file_path = images_dir / unique_filename
+
+ # Сохраняем файл
+ try:
+ with open(file_path, "wb") as f:
+ content = await file.read()
+ f.write(content)
+
+ # Обновляем все службы этой категории
+ async with AsyncSessionLocal() as session:
+ await session.execute(
+ update(Service)
+ .where(Service.category == category)
+ .values(image_path=str(file_path))
+ )
+ await session.commit()
+
+ return JSONResponse({
+ "success": True,
+ "message": f"Картинка загружена",
+ "file_path": str(file_path)
+ })
+ except Exception as e:
+ logger.error(f"Ошибка загрузки картинки: {e}")
+ return JSONResponse({
+ "success": False,
+ "error": str(e)
+ }, status_code=500)
+
+
+@app.post("/api/service/{service_id}/update")
+async def api_update_service(service_id: int, request: Request,
+ username: str = Depends(get_current_admin)):
+ """Обновить службу"""
+ data = await request.json()
+ async with AsyncSessionLocal() as session:
+ await session.execute(update(Service).where(Service.id == service_id).values(**data))
+ await session.commit()
+ return JSONResponse({"success": True})
+
+
+@app.post("/api/service/{service_id}/delete")
+async def api_delete_service(service_id: int, username: str = Depends(get_current_admin)):
+ """Удалить службу"""
+ async with AsyncSessionLocal() as session:
+ await session.execute(delete(Service).where(Service.service_id == service_id))
+ await session.commit()
+ return JSONResponse({"success": True})
+
+
+# ============================================================================
+# ОБЪЯВЛЕНИЯ
+# ============================================================================
+
+@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("ads.html", {
+ "request": request, "username": username, "ads": ads
+ })
+
+
+@app.post("/api/ad/add")
+async def api_add_ad(request: Request, username: str = Depends(get_current_admin)):
+ """Добавить объявление"""
+ data = await request.json()
+ async with AsyncSessionLocal() as session:
+ ad = Ad(
+ title=data['title'],
+ description=data.get('text', ''),
+ category=data.get('category', 'other'),
+ user_id=config.ADMIN_USER_ID,
+ is_active=data.get('is_active', True),
+ is_moderated=True
+ )
+ session.add(ad)
+ await session.commit()
+ return JSONResponse({"success": True, "message": "Объявление добавлено"})
+
+
+@app.post("/api/ad/{ad_id}/delete")
+async def api_delete_ad(ad_id: int, username: str = Depends(get_current_admin)):
+ """Удалить объявление"""
+ async with AsyncSessionLocal() as session:
+ await session.execute(update(Ad).where(Ad.ad_id == ad_id).values(is_active=False))
+ await session.commit()
+ return JSONResponse({"success": True})
+
+
+# ============================================================================
+# РАССЫЛКИ — единая страница создания и статистики
+# ============================================================================
+
+@app.get("/broadcast", response_class=HTMLResponse)
+async def broadcast_page(request: Request, username: str = Depends(get_current_admin)):
+ """Рассылки — создание и статистика"""
+ from database.models import Broadcast, BroadcastRead
+ from sqlalchemy import func
+
+ async with AsyncSessionLocal() as session:
+ # История рассылок
+ stmt = select(Broadcast).order_by(Broadcast.created_at.desc()).limit(50)
+ result = await session.execute(stmt)
+ broadcasts = list(result.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,
+ 'unread_count': b.total_sent - read_count,
+ 'read_percent': round((read_count / b.total_sent * 100), 1) if b.total_sent > 0 else 0,
+ 'is_reminder_sent': b.is_reminder_sent,
+ 'broadcast_type': b.broadcast_type or 'regular',
+ })
+
+ return templates.TemplateResponse("broadcast.html", {
+ "request": request,
+ "username": username,
+ "broadcasts": broadcasts_data,
+ })
+
+
+# ============================================================================
+# ЛОГИРОВАНИЕ АКТИВНОСТИ
+# ============================================================================
+
+@app.get("/api/activity_log")
+async def api_get_activity_log(
+ username: str = Depends(get_current_admin),
+ action_type: str = "",
+ days: int = 30
+):
+ """Получить логи активности"""
+ from sqlalchemy import text, desc
+
+ async with AsyncSessionLocal() as session:
+ stmt = text('''
+ SELECT * FROM activity_log
+ WHERE (:action_type = '' OR action_type = :action_type)
+ AND created_at >= datetime('now', '-' || :days || ' days')
+ ORDER BY created_at DESC
+ LIMIT 100
+ ''')
+
+ result = await session.execute(stmt, {"action_type": action_type, "days": str(days)})
+ logs = [
+ {
+ "id": row[0],
+ "user_id": row[1],
+ "action_type": row[2],
+ "action_data": row[3],
+ "recipients_count": row[4],
+ "created_at": row[5]
+ }
+ for row in result.all()
+ ]
+
+ return JSONResponse({"success": True, "logs": logs})
+
+
+# ============================================================================
+# ОПРОСЫ
+# ============================================================================
+
+@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("polls.html", {
+ "request": request, "username": username, "polls": polls
+ })
+
+
+@app.post("/api/poll/create")
+async def api_create_poll(request: Request, username: str = Depends(get_current_admin)):
+ """Создать опрос"""
+ data = await request.json()
+ async with AsyncSessionLocal() as session:
+ poll = Poll(
+ question=data['question'],
+ options=json.dumps(data['options']),
+ votes='{}',
+ created_by=config.ADMIN_USER_ID,
+ is_active=data.get('is_active', True),
+ message_id=0, # Заглушка
+ chat_id=config.ADMIN_CHAT_ID
+ )
+ session.add(poll)
+ await session.commit()
+ return JSONResponse({"success": True, "message": "Опрос создан"})
+
+
+@app.post("/api/poll/{poll_id}/close")
+async def api_close_poll(poll_id: int, username: str = Depends(get_current_admin)):
+ """Закрыть опрос"""
+ async with AsyncSessionLocal() as session:
+ await session.execute(update(Poll).where(Poll.poll_id == poll_id).values(is_active=False))
+ await session.commit()
+ return JSONResponse({"success": True})
+
+
+# ============================================================================
+# СОБЫТИЯ
+# ============================================================================
+
+@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("events.html", {
+ "request": request, "username": username, "events": events
+ })
+
+
+@app.post("/api/event/add")
+async def api_add_event(request: Request, username: str = Depends(get_current_admin)):
+ """Добавить событие"""
+ data = await request.json()
+ async with AsyncSessionLocal() as session:
+ event = Event(
+ title=data['title'], description=data.get('description', ''),
+ event_type=data.get('event_type', 'meeting'),
+ event_date=datetime.fromisoformat(data['event_date']),
+ created_by=config.ADMIN_USER_ID,
+ is_active=data.get('is_active', True)
+ )
+ session.add(event)
+ await session.commit()
+ return JSONResponse({"success": True, "message": "Событие добавлено"})
+
+
+# ============================================================================
+# РАСПИСАНИЯ
+# ============================================================================
+
+@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("schedules.html", {
+ "request": request, "username": username, "schedules": schedules
+ })
+
+
+@app.post("/api/schedule/add")
+async def api_add_schedule(request: Request, username: str = Depends(get_current_admin)):
+ """Добавить расписание"""
+ data = await request.json()
+ async with AsyncSessionLocal() as session:
+ schedule = Schedule(
+ title=data['title'], schedule_type=data.get('schedule_type', 'water'),
+ start_time=datetime.fromisoformat(data['start_time']),
+ end_time=datetime.fromisoformat(data['end_time']) if data.get('end_time') else None,
+ description=data.get('description', ''), is_active=data.get('is_active', True)
+ )
+ session.add(schedule)
+ await session.commit()
+ return JSONResponse({"success": True, "message": "Расписание добавлено"})
+
+
+@app.post("/api/schedule/{schedule_id}/delete")
+async def api_delete_schedule(schedule_id: int, username: str = Depends(get_current_admin)):
+ """Удалить расписание"""
+ async with AsyncSessionLocal() as session:
+ await session.execute(update(Schedule).where(
+ Schedule.schedule_id == schedule_id).values(is_active=False))
+ await session.commit()
+ return JSONResponse({"success": True})
+
+
+# ============================================================================
+# ЭКСПОРТ
+# ============================================================================
+
+@app.get("/export", response_class=HTMLResponse)
+async def export_page(request: Request, username: str = Depends(get_current_admin)):
+ """Экспорт"""
+ return templates.TemplateResponse("export.html", {
+ "request": request, "username": username
+ })
+
+
+@app.get("/api/export/json")
+async def api_export_json(username: str = Depends(get_current_admin)):
+ """Экспорт JSON"""
+ from services.analytics import ChatAnalytics
+ async with AsyncSessionLocal() as session:
+ analytics = ChatAnalytics(session)
+ filepath = await analytics.export_to_json()
+ return JSONResponse({"success": True, "filepath": filepath})
+
+
+# ============================================================================
+# ЗАПЛАНИРОВАННЫЕ ПОСТЫ
+# ============================================================================
+
+@app.get("/scheduled_posts", response_class=HTMLResponse)
+async def scheduled_posts_page(request: Request, username: str = Depends(get_current_admin)):
+ """Страница запланированных постов"""
+ return templates.TemplateResponse("scheduled_posts.html", {
+ "request": request,
+ "username": username
+ })
+
+
+@app.post("/api/scheduled_posts/create")
+async def api_create_scheduled_post(
+ text: str = Form(...),
+ topic_name: str = Form(...),
+ recipients: str = Form("chat_only"),
+ scheduled_time: str = Form(...),
+ photo: UploadFile = File(None),
+ document: UploadFile = File(None), # Новый параметр — документ
+ username: str = Depends(get_current_admin)
+):
+ """Создать запланированный пост (с фото ИЛИ документом)"""
+ from datetime import datetime
+ from config import ADMIN_CHAT_ID, get_topic_id, get_proxy_url
+ import aiohttp
+
+ logger.info(f"📅 СОЗДАНИЕ ЗАПЛАНИРОВАННОГО ПОСТА: тема={topic_name}, время={scheduled_time}")
+
+ try:
+ # Парсим время
+ scheduled_dt = datetime.fromisoformat(scheduled_time)
+
+ # Если нет timezone — считаем что это локальное время (UTC+4)
+ # и конвертируем в UTC для scheduler
+ if scheduled_dt.tzinfo is None:
+ from datetime import timezone as tz
+ local_tz = tz(timedelta(hours=4)) # Ульяновск UTC+4
+ scheduled_dt = scheduled_dt.replace(tzinfo=local_tz) # Помечаем как локальное
+ scheduled_dt = scheduled_dt.astimezone(tz.utc).replace(tzinfo=None) # Конвертируем в UTC
+ logger.info(f"⏰ Время конвертирована из локального в UTC: {scheduled_dt}")
+ else:
+ scheduled_dt = scheduled_dt.astimezone(tz.utc).replace(tzinfo=None)
+ logger.info(f"⏰ Время уже с timezone, конвертирована в UTC: {scheduled_dt}")
+
+ # Проверяем что время в будущем (по UTC)
+ if scheduled_dt < datetime.utcnow():
+ return JSONResponse({
+ "success": False,
+ "error": "Время должно быть в будущем"
+ }, status_code=400)
+
+ # Получаем topic_id из topic_name
+ topic_id = get_topic_id(topic_name)
+
+ # Если есть фото - загружаем в Telegram
+ photo_file_id = None
+ if photo and photo.filename:
+ temp_path = Path("data") / f"scheduled_post_{photo.filename}"
+ with open(temp_path, "wb") as f:
+ content = await photo.read()
+ f.write(content)
+
+ try:
+ 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'), filename=photo.filename)
+ data.add_field('caption', 'Preview')
+
+ async with session.post(url, data=data, proxy=get_proxy_url()) as resp:
+ result = await resp.json()
+ if result.get('ok'):
+ photo_file_id = result['result']['photo'][-1]['file_id']
+ except Exception as e:
+ logger.error(f"Ошибка загрузки фото: {e}")
+ photo_file_id = None
+
+ # Если есть документ - загружаем в Telegram
+ document_file_id = None
+ document_name = None
+ if document and document.filename:
+ # Проверяем расширение
+ allowed_ext = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt', '.zip', '.rar']
+ file_ext = Path(document.filename).suffix.lower()
+ if file_ext not in allowed_ext:
+ return JSONResponse({
+ "success": False,
+ "error": f"Неподдерживаемый формат файла. Разрешены: {', '.join(allowed_ext)}"
+ }, status_code=400)
+
+ # Сохраняем временно
+ temp_path = Path("data") / f"scheduled_doc_{document.filename}"
+ with open(temp_path, "wb") as f:
+ content = await document.read()
+ f.write(content)
+
+ try:
+ async with aiohttp.ClientSession() as session:
+ url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/sendDocument"
+ data = aiohttp.FormData()
+ data.add_field('chat_id', str(config.ADMIN_USER_ID))
+
+ with open(temp_path, 'rb') as doc_file:
+ data.add_field('document', doc_file, filename=document.filename)
+ data.add_field('caption', 'Document preview')
+
+ async with session.post(url, data=data, proxy=get_proxy_url()) as resp:
+ result = await resp.json()
+ if result.get('ok'):
+ document_file_id = result['result']['document']['file_id']
+ document_name = document.filename
+ logger.info(f"✅ Документ загружен: {document_name}, file_id={document_file_id}")
+ else:
+ error_msg = result.get('description', 'Неизвестная ошибка')
+ logger.error(f"Ошибка загрузки документа: {error_msg}")
+ except Exception as e:
+ logger.error(f"Ошибка загрузки документа: {e}")
+ document_file_id = None
+ document_name = None
+
+ # Удаляем временный файл
+ if temp_path.exists():
+ temp_path.unlink()
+
+ # Сохраняем в БД
+ async with AsyncSessionLocal() as session:
+ scheduled_post = ScheduledPost(
+ text=text,
+ photo_file_id=photo_file_id,
+ document_file_id=document_file_id,
+ document_name=document_name,
+ topic_id=topic_id,
+ topic_name=topic_name,
+ recipients=recipients,
+ scheduled_time=scheduled_dt,
+ status='pending',
+ created_by=config.ADMIN_USER_ID
+ )
+ session.add(scheduled_post)
+ await session.commit()
+ await session.refresh(scheduled_post)
+
+ logger.info(f"✅ Пост создан: id={scheduled_post.id}")
+
+ attachment_info = ""
+ if document_name:
+ attachment_info = f" + 📄 {document_name}"
+ elif photo_file_id:
+ attachment_info = " + 📷 фото"
+
+ return JSONResponse({
+ "success": True,
+ "message": f"Пост запланирован на {scheduled_dt.strftime('%d.%m.%Y %H:%M')}{attachment_info}",
+ "post_id": scheduled_post.id
+ })
+
+ except ValueError as e:
+ return JSONResponse({
+ "success": False,
+ "error": f"Неверный формат времени: {e}"
+ }, status_code=400)
+ except Exception as e:
+ logger.error(f"Ошибка создания поста: {e}")
+ return JSONResponse({
+ "success": False,
+ "error": str(e)
+ }, status_code=500)
+
+
+@app.get("/api/scheduled_posts/list")
+async def api_list_scheduled_posts(username: str = Depends(get_current_admin)):
+ """Список запланированных постов"""
+ from datetime import timedelta
+
+ async with AsyncSessionLocal() as session:
+ stmt = select(ScheduledPost).order_by(ScheduledPost.scheduled_time.desc())
+ result = await session.execute(stmt)
+ posts = list(result.scalars().all())
+
+ # Часовой пояс пользователя (UTC+4 для Ульяновска)
+ user_tz_offset = timedelta(hours=4) # Europe/Ulyanovsk
+
+ posts_data = []
+ for post in posts:
+ # Конвертируем UTC в локальное время
+ local_time = post.scheduled_time + user_tz_offset
+
+ 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": local_time.strftime('%d.%m.%Y %H:%M'), # Уже локальное!
+ "status": post.status,
+ "status_emoji": post.get_status_emoji(),
+ "created_at": fmt_local(post.created_at),
+ "has_photo": bool(post.photo_file_id)
+ })
+
+ return JSONResponse({
+ "success": True,
+ "posts": posts_data,
+ "timezone": "UTC+4 (Ульяновск/Самара)"
+ })
+
+
+@app.post("/api/scheduled_posts/{post_id}/cancel")
+async def api_cancel_scheduled_post(post_id: int, username: str = Depends(get_current_admin)):
+ """Отменить запланированный пост"""
+ async with AsyncSessionLocal() as session:
+ stmt = select(ScheduledPost).where(ScheduledPost.id == post_id)
+ result = await session.execute(stmt)
+ post = result.scalar_one_or_none()
+
+ if not post:
+ return JSONResponse({
+ "success": False,
+ "error": "Пост не найден"
+ }, status_code=404)
+
+ if post.status != 'pending':
+ return JSONResponse({
+ "success": False,
+ "error": f"Нельзя отменить пост со статусом {post.status}"
+ }, status_code=400)
+
+ post.status = 'cancelled'
+ await session.commit()
+
+ logger.info(f"🚫 Пост отменён: id={post_id}")
+
+ return JSONResponse({
+ "success": True,
+ "message": "Пост отменён"
+ })
+
+
+# ============================================================================
+# НАСТРОЙКИ (перенесено в /broadcast)
+# ============================================================================
+# Настройка лимита рассылки ИГ теперь в /broadcast
+
+
+@app.post("/api/settings/ig_broadcast_limit")
+async def api_set_ig_broadcast_limit(
+ request: Request,
+ username: str = Depends(get_current_admin)
+):
+ """Установить лимит рассылки ИГ"""
+ from sqlalchemy import text
+ data = await request.json()
+ limit = data.get('limit', 0)
+
+ async with AsyncSessionLocal() as session:
+ await session.execute(text('''
+ INSERT OR REPLACE INTO settings (key, value, description)
+ VALUES ('ig_broadcast_limit', :limit, 'Лимит рассылки ИГ в секундах')
+ '''), {"limit": str(limit)})
+ await session.commit()
+
+ return JSONResponse({"success": True, "message": f"Лимит установлен: {limit} сек"})
+
+
+@app.get("/api/settings/ig_broadcast_limit")
+async def api_get_ig_broadcast_limit(
+ username: str = Depends(get_current_admin)
+):
+ """Получить лимит рассылки ИГ"""
+ from sqlalchemy import text
+
+ async with AsyncSessionLocal() as session:
+ result = await session.execute(text('''
+ SELECT value FROM settings WHERE key = 'ig_broadcast_limit'
+ '''))
+ row = result.first()
+
+ if row:
+ return JSONResponse({"success": True, "value": row[0]})
+ else:
+ return JSONResponse({"success": True, "value": "0"})
+
+
+def run_web_server(host: str = "0.0.0.0", port: int = 8000):
+ """Запуск веб-сервера"""
+ import uvicorn
+ uvicorn.run(app, host=host, port=port)
+
+
+
+@app.post("/api/broadcast/send_reminder/{broadcast_id}")
+async def api_send_broadcast_reminder(broadcast_id: int, username: str = Depends(get_current_admin)):
+ """Отправить напоминание непрочитавшим"""
+ from handlers.smart_broadcast import send_reminder_to_unread
+ from bot_instance import get_bot
+
+ bot = get_bot()
+ sent_count = await send_reminder_to_unread(bot, broadcast_id)
+
+ return JSONResponse({
+ "success": True,
+ "message": f"Напоминание отправлено {sent_count} пользователям"
+ })
+
+
+@app.get("/api/broadcasts/list")
+async def api_get_broadcasts_list(limit: int = 10, username: str = Depends(get_current_admin)):
+ """Получить список последних рассылок"""
+ from database.models import Broadcast, BroadcastRead
+ from sqlalchemy import func, desc
+
+ async with AsyncSessionLocal() as session:
+ stmt = select(Broadcast).order_by(Broadcast.created_at.desc()).limit(limit)
+ result = await session.execute(stmt)
+ broadcasts = list(result.scalars().all())
+
+ total = (await session.execute(select(func.count(Broadcast.id)))).scalar() or 0
+
+ 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,
+ '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,
+ })
+
+ return JSONResponse({
+ "success": True,
+ "total": total,
+ "broadcasts": broadcasts_data
+ })
+
+
+@app.post("/api/broadcast/create")
+async def api_create_broadcast(
+ text: str = Form(...),
+ photo: UploadFile = File(None),
+ recipients: str = Form("all_and_chat"),
+ has_read_button: bool = Form(True),
+ dry_run: bool = Form(False), # Режим теста БЕЗ отправки
+ username: str = Depends(get_current_admin)
+):
+ """Создать и отправить рассылку из веб-панели"""
+
+ # dry_run = тестирование без реальной отправки
+ if dry_run:
+ from database.models import User
+ from services.initiative_group import InitiativeGroupService
+
+ async with AsyncSessionLocal() as session:
+ if recipients == 'ig_only':
+ ig_service = InitiativeGroupService(session)
+ ig_user_ids = await ig_service.get_member_ids(active_only=True)
+ stmt = select(User).where(User.user_id.in_(ig_user_ids))
+ elif recipients == 'all_verified':
+ stmt = select(User).where(User.verified == True)
+ else:
+ stmt = select(User).where(User.verified == True)
+
+ result = await session.execute(stmt)
+ users = list(result.scalars().all())
+
+ logger.info(f"🧪 DRY RUN рассылки: получатели={recipients}, пользователей={len(users)}")
+ return JSONResponse({
+ "success": True,
+ "dry_run": True,
+ "message": f"Тестовый режим. Рассылка получила бы {len(users)} пользователей",
+ "recipients_count": len(users)
+ })
+
+ import aiohttp
+ from database.models import Broadcast
+ from datetime import datetime
+
+ photo_file_id = None
+
+ # Если есть фото - загружаем через Telegram Bot API
+ if photo and photo.filename:
+ try:
+ # Сохраняем временно
+ temp_path = Path("data") / f"broadcast_{photo.filename}"
+ content = await photo.read()
+ with open(temp_path, "wb") as f:
+ f.write(content)
+
+ # Загружаем фото через Telegram API для получения file_id
+ async with aiohttp.ClientSession() as session:
+ url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/sendPhoto"
+
+ # Если бот использует прокси - добавляем его в aiohttp
+ proxy = config.get_proxy_url()
+
+ data = aiohttp.FormData()
+ data.add_field('chat_id', config.ADMIN_USER_ID)
+
+ # Открываем файл и сразу передаём в FormData
+ with open(temp_path, 'rb') as photo_file:
+ data.add_field('photo', photo_file, filename=photo.filename)
+ data.add_field('caption', 'broadcast_test')
+ data.add_field('parse_mode', 'HTML')
+
+ async with session.post(url, data=data, proxy=proxy) as resp:
+ text = await resp.text()
+
+ # Проверяем что ответ успешный
+ if resp.status != 200:
+ logger.error(f"Ошибка загрузки фото: HTTP {resp.status}, ответ: {text}")
+ raise Exception(f"Telegram API вернул ошибку: HTTP {resp.status}")
+
+ # Пытаемся распарсить JSON
+ try:
+ result = json.loads(text)
+ except json.JSONDecodeError as e:
+ logger.error(f"Ошибка парсинга JSON от Telegram API: {e}, ответ: {text}")
+ raise Exception(f"Telegram API вернул не-JSON ответ: {text[:100]}")
+
+ if result.get('ok'):
+ photo_file_id = result['result']['photo'][-1]['file_id']
+ logger.info(f"✅ Фото загружено, file_id: {photo_file_id}")
+ else:
+ error_msg = result.get('description', 'Неизвестная ошибка')
+ logger.error(f"Ошибка загрузки фото: {error_msg}")
+ raise Exception(f"Ошибка Telegram API: {error_msg}")
+
+ # Удаляем временный файл
+ if temp_path.exists():
+ temp_path.unlink()
+
+ except Exception as e:
+ logger.error(f"❌ Ошибка загрузки фото для рассылки: {e}")
+ # Не падаем, просто продолжаем без фото
+ photo_file_id = None
+
+
+ # Получаем бота из глобального экземпляра
+ from bot_instance import get_bot
+ bot = get_bot()
+
+ # Отправляем рассылку
+ try:
+ from handlers.smart_broadcast import send_smart_broadcast
+ success = await send_smart_broadcast(bot, text, recipients, photo_file_id)
+
+ if success:
+ return JSONResponse({
+ "success": True,
+ "message": f"Рассылка отправлена получателям: {recipients}"
+ })
+ else:
+ return JSONResponse({
+ "error": "Ошибка при отправке рассылки. Проверьте логи бота."
+ }, status_code=500)
+ except Exception as e:
+ logger.error(f"❌ Критическая ошибка при отправке рассылки: {e}")
+ import traceback
+ traceback.print_exc()
+ return JSONResponse({
+ "error": f"Критическая ошибка: {str(e)}"
+ }, status_code=500)
+
+
+@app.get("/api/broadcast/readers/{broadcast_id}")
+async def api_get_broadcast_readers(broadcast_id: int, username: str = Depends(get_current_admin)):
+ """Получить список прочитавших"""
+ from database.models import BroadcastRead, User
+
+ async with AsyncSessionLocal() as session:
+ stmt = (
+ select(User, BroadcastRead.read_at)
+ .join(BroadcastRead, User.user_id == BroadcastRead.user_id)
+ .where(BroadcastRead.broadcast_id == broadcast_id)
+ .order_by(BroadcastRead.read_at.desc())
+ )
+ result = await session.execute(stmt)
+ rows = result.all()
+
+ readers = [
+ {
+ 'user_id': user.user_id,
+ 'name': user.full_name,
+ 'apartment': user.apartment,
+ 'read_at': fmt_local(read_at),
+ }
+ for user, read_at in rows
+ ]
+
+ return JSONResponse({"success": True, "readers": readers})
+
+
+@app.get("/api/broadcast/non_readers/{broadcast_id}")
+async def api_get_broadcast_non_readers(broadcast_id: int, username: str = Depends(get_current_admin)):
+ """Получить список НЕ прочитавших"""
+ from database.models import BroadcastRead, User
+ from sqlalchemy import select as sql_select, not_
+
+ async with AsyncSessionLocal() as session:
+ # Кто прочитал
+ read_stmt = select(BroadcastRead.user_id).where(BroadcastRead.broadcast_id == broadcast_id)
+ read_result = await session.execute(read_stmt)
+ read_user_ids = set(row[0] for row in read_result.all())
+
+ # Все верифицированные
+ all_stmt = select(User).where(User.verified == True)
+ all_result = await session.execute(all_stmt)
+ all_users = list(all_result.scalars().all())
+
+ # Кто НЕ прочитал
+ non_readers = [
+ {
+ 'user_id': u.user_id,
+ 'name': u.full_name,
+ 'apartment': u.apartment,
+ }
+ for u in all_users if u.user_id not in read_user_ids
+ ]
+
+ return JSONResponse({"success": True, "non_readers": non_readers})
+
+
+# ============================================================================
+# V3.1 - ДАЙДЖЕСТЫ
+# ============================================================================
+
+@app.post("/api/digest/generate")
+async def api_generate_digest(username: str = Depends(get_current_admin)):
+ """Сгенерировать дайджест прямо сейчас (из веб-панели)"""
+ from services.digest_service import DigestService
+ from database.models import Digest
+ from datetime import date
+
+ try:
+ async with AsyncSessionLocal() as session:
+ service = DigestService(session)
+
+ # Генерируем
+ digest_data = await service.generate_weekly_digest()
+
+ if not digest_data:
+ return JSONResponse({
+ "success": False,
+ "error": "Не удалось сгенерировать дайджест. Проверьте логи."
+ }, status_code=500)
+
+ # Проверяем нет ли уже за эту неделю
+ check_stmt = select(Digest).where(
+ Digest.week_number == digest_data['week_number'],
+ Digest.year == digest_data['year']
+ )
+ result = await session.execute(check_stmt)
+ existing = result.scalar_one_or_none()
+
+ if existing:
+ return JSONResponse({
+ "success": True,
+ "message": f"Дайджест за неделю #{digest_data['week_number']}/{digest_data['year']} уже существует (ID: #{existing.id}). Вы можете отправить его в разделе 'Архив'.",
+ "digest_id": existing.id,
+ "already_exists": True
+ })
+
+ # Сохраняем
+ digest_id = await service.save_digest(digest_data, status='pending_approval')
+
+ # Отправляем превью админу в личку через HTTP
+ from config import get_proxy_url
+
+ async with aiohttp.ClientSession() as http_session:
+ url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/sendMessage"
+ keyboard = {
+ "inline_keyboard": [
+ [{"text": "🌐 Утвердить в веб-панели", "url": "http://localhost:8000/digests"}]
+ ]
+ }
+ payload = {
+ 'chat_id': config.ADMIN_USER_ID,
+ 'text': f"📰 Дайджест за неделю готов!\n\n"
+ f"Неделя #{digest_data['week_number']}/{digest_data['year']}\n"
+ f"Период: {digest_data['period_start'].strftime('%d.%m')} - {digest_data['period_end'].strftime('%d.%m.%Y')}\n\n"
+ f"Зайдите в веб-панель для утверждения:\n"
+ f"http://localhost:8000/digests",
+ 'parse_mode': 'HTML',
+ 'reply_markup': json.dumps(keyboard)
+ }
+ async with http_session.post(url, json=payload, proxy=get_proxy_url()) as resp:
+ await resp.json()
+
+ return JSONResponse({
+ "success": True,
+ "message": f"✅ Дайджест #{digest_id} сгенерирован! Превью отправлено вам в Telegram.",
+ "digest_id": digest_id
+ })
+
+ except Exception as e:
+ import traceback
+ logger.error(f"Ошибка генерации дайджеста: {e}")
+ traceback.print_exc()
+ return JSONResponse({
+ "success": False,
+ "error": f"Критическая ошибка: {str(e)}"
+ }, status_code=500)
+
+
+@app.get("/digests", response_class=HTMLResponse)
+async def digests_page(request: Request, username: str = Depends(get_current_admin)):
+ """Страница дайджестов"""
+ from services.digest_service import DigestService
+
+ async with AsyncSessionLocal() as session:
+ service = DigestService(session)
+ pending = await service.get_pending_digests()
+ archive = await service.get_digest_archive()
+
+ return templates.TemplateResponse("digests.html", {
+ "request": request,
+ "username": username,
+ "pending_digests": pending,
+ "archive_digests": archive,
+ })
+
+
+@app.post("/api/digest/{digest_id}/approve")
+async def api_approve_digest(digest_id: int, username: str = Depends(get_current_admin)):
+ """Утвердить дайджест"""
+ from services.digest_service import DigestService
+
+ async with AsyncSessionLocal() as session:
+ service = DigestService(session)
+ success = await service.update_digest_status(
+ digest_id,
+ 'approved',
+ approved_by=config.ADMIN_USER_ID
+ )
+
+ if success:
+ return JSONResponse({"success": True, "message": "Дайджест утверждён"})
+ else:
+ return JSONResponse({"error": "Ошибка утверждения"}, status_code=500)
+
+
+@app.post("/api/digest/{digest_id}/reject")
+async def api_reject_digest(digest_id: int, request: Request, username: str = Depends(get_current_admin)):
+ """Отклонить дайджест"""
+ from services.digest_service import DigestService
+
+ data = await request.json()
+ reason = data.get('reason', 'Не указана')
+
+ async with AsyncSessionLocal() as session:
+ service = DigestService(session)
+ success = await service.update_digest_status(
+ digest_id,
+ 'rejected',
+ rejected_by=config.ADMIN_USER_ID,
+ rejection_reason=reason
+ )
+
+ if success:
+ return JSONResponse({"success": True, "message": "Дайджест отклонён"})
+ else:
+ return JSONResponse({"error": "Ошибка отклонения"}, status_code=500)
+
+
+@app.post("/api/digest/{digest_id}/send")
+async def api_send_digest(digest_id: int, username: str = Depends(get_current_admin)):
+ """Отправить утверждённый дайджест"""
+ from services.digest_service import DigestService
+ from database.models import User, Digest
+ import aiohttp
+
+ async with AsyncSessionLocal() as session:
+ # Получаем дайджест
+ digest = await session.get(Digest, digest_id)
+ if not digest or digest.status != 'approved':
+ return JSONResponse({"error": "Дайджест не утверждён"}, status_code=400)
+
+ # Получаем всех верифицированных
+ stmt = select(User).where(User.verified == True)
+ result = await session.execute(stmt)
+ users = list(result.scalars().all())
+
+ text = digest.digest_text
+ sent_count = 0
+ error_count = 0
+ message_id = None
+
+ # Сначала в чат
+ try:
+ async with aiohttp.ClientSession() as http_session:
+ url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/sendMessage"
+ params = {
+ 'chat_id': config.ADMIN_CHAT_ID,
+ 'text': f"📰 Еженедельный дайджест\n\n{text}",
+ 'parse_mode': 'HTML'
+ }
+ async with http_session.post(url, json=params, timeout=30) as resp:
+ if resp.status == 200:
+ resp_data = await resp.json()
+ message_id = resp_data['result']['message_id']
+ except Exception as e:
+ logger.error(f"Ошибка отправки в чат: {e}")
+
+ # Потом пользователям
+ async with aiohttp.ClientSession() as http_session:
+ for user in users:
+ try:
+ url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/sendMessage"
+ params = {
+ 'chat_id': user.user_id,
+ 'text': f"📰 Еженедельный дайджест\n\n{text}",
+ 'parse_mode': 'HTML'
+ }
+ async with http_session.post(url, json=params, timeout=10) as resp:
+ if resp.status == 200:
+ sent_count += 1
+ except Exception as e:
+ logger.error(f"Ошибка отправки пользователю {user.user_id}: {e}")
+ error_count += 1
+
+ # Обновляем статус
+ service = DigestService(session)
+ await service.update_digest_status(digest_id, 'sent', message_id=message_id)
+
+ return JSONResponse({
+ "success": True,
+ "message": f"Дайджест отправлен {sent_count} пользователям, ошибок: {error_count}"
+ })
+
+
+@app.get("/api/digest/{digest_id}")
+async def api_get_digest(digest_id: int, username: str = Depends(get_current_admin)):
+ """Получить данные дайджеста"""
+ from database.models import Digest
+
+ async with AsyncSessionLocal() as session:
+ digest = await session.get(Digest, digest_id)
+
+ if not digest:
+ return JSONResponse({"error": "Дайджест не найден"}, status_code=404)
+
+ return JSONResponse({
+ "digest": {
+ "id": digest.id,
+ "week": digest.week_number,
+ "year": digest.year,
+ "period_label": digest.period_label,
+ "status": digest.status,
+ "text": digest.digest_text or '',
+ "created_at": fmt_local(digest.created_at),
+ }
+ })
+
+
+@app.post("/api/digest/{digest_id}/update_text")
+async def api_update_digest_text(digest_id: int, request: Request, username: str = Depends(get_current_admin)):
+ """Обновить текст дайджеста"""
+ from services.digest_service import DigestService
+
+ data = await request.json()
+ new_text = data.get('text', '')
+
+ async with AsyncSessionLocal() as session:
+ service = DigestService(session)
+ success = await service.update_digest_text(digest_id, new_text)
+
+ if success:
+ return JSONResponse({"success": True, "message": "Текст обновлён"})
+ else:
+ return JSONResponse({"error": "Ошибка обновления текста"}, status_code=500)
+
+
+@app.get("/digest_settings", response_class=HTMLResponse)
+async def digest_settings_page(request: Request, username: str = Depends(get_current_admin)):
+ """Страница настроек дайджеста"""
+ return templates.TemplateResponse("digest_settings.html", {
+ "request": request,
+ "username": username,
+ })
+
+
+if __name__ == "__main__":
+ run_web_server()
diff --git a/web/templates/digests.html b/web/templates/digests.html
index 1dbf166..a0a8748 100644
--- a/web/templates/digests.html
+++ b/web/templates/digests.html
@@ -68,11 +68,11 @@
{% if pending_digests|length > 0 %}
{% for digest in pending_digests %}
Экспорт всех пользователей и сообщений в JSON формат.
+Экспорт ВСЕХ пользователей и сообщений. Файл будет автоматически отправлен в Gitea.
Экспорт включает:
-Последняя выгрузка:
+ {{ fmt_local(export_stats.last_date) if export_stats.last_date else 'Никогда' }}
- Файл сохраняется в директории exports/
+ Автоматический экспорт выполняется раз в неделю (вс, 03:00) и пушится в репозиторий.