v1.19 ИГ рассылка + объявления 14 дней + настройки
🔥 ОБЪЯВЛЕНИЯ: • Срок действия: 7 → 14 дней 🎯 ИНИЦИАТИВНАЯ ГРУППА: • Добавлена кнопка 'Рассылка ИГ' в меню • Команда /ig_broadcast для членов ИГ • Поддержка текста + фото + файлов • Настройка лимита в веб-панели ⚙️ ВЕБ-ПАНЕЛЬ: • Новая страница /settings • Настройка лимита рассылки ИГ (секунды) • Кнопки статистики фильтруют пользователей • Индикатор активного фильтра 🎨 UI: • Исправлен <font> → emoji (Telegram не поддерживает) • Цветные заголовки через emoji 📁 ФАЙЛЫ: • handlers/ads.py: 14 дней • handlers/initiative_group.py: рассылка ИГ • web/app.py: API настроек • web/templates/settings.html: страница настроек • utils/ui_styles.py: без <font> тегов • database: settings table Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
da010eccbf
commit
cef2ac3714
24 changed files with 3568 additions and 220 deletions
296
ACCESS_LEVELS_TABLE.md
Normal file
296
ACCESS_LEVELS_TABLE.md
Normal file
|
|
@ -0,0 +1,296 @@
|
||||||
|
# 📊 ТАБЛИЦА ВОЗМОЖНОСТЕЙ БОТА
|
||||||
|
|
||||||
|
**Обновлено:** 5 марта 2026
|
||||||
|
**Версия:** v1.15
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👤 ТВОЙ СТАТУС
|
||||||
|
|
||||||
|
| Параметр | Значение |
|
||||||
|
|----------|----------|
|
||||||
|
| **Telegram ID** | `197957361` |
|
||||||
|
| **Имя** | Александр MatrixHasYou |
|
||||||
|
| **Статус** | **СУПЕР-АДМИН** (владелец бота) |
|
||||||
|
| **Владеешь ботом** | ✅ ДА (BOT_TOKEN у тебя) |
|
||||||
|
| **Админ в боте** | ✅ ДА (`config.ADMIN_USER_ID`) |
|
||||||
|
| **Веб-админка** | ✅ ДА (логин: `MatrixHasYou`) |
|
||||||
|
| **Доступ к серверу** | ✅ ДА (dockerserver) |
|
||||||
|
|
||||||
|
**Ты выше обычного админа!** У тебя есть:
|
||||||
|
- ✅ Все права админа
|
||||||
|
- ✅ Доступ к `.env` и токенам
|
||||||
|
- ✅ Доступ к базе данных
|
||||||
|
- ✅ Доступ к логам
|
||||||
|
- ✅ Право останавливать/запускать бота
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 УРОВНИ ДОСТУПА
|
||||||
|
|
||||||
|
| Уровень | Кто | Как получить |
|
||||||
|
|---------|-----|--------------|
|
||||||
|
| **0** | Неверифицированный | Автоматически при `/start` |
|
||||||
|
| **1** | Верифицированный | Отправить номер квартиры → админ подтвердил |
|
||||||
|
| **2** | Инициативная группа | Админ добавил: `/ig_add <user_id>` |
|
||||||
|
| **3** | Админ | `is_admin=True` в базе или `ADMIN_USER_ID` |
|
||||||
|
| **4** | Супер-админ | `config.ADMIN_USER_ID` (ты) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 ТАБЛИЦА ВОЗМОЖНОСТЕЙ
|
||||||
|
|
||||||
|
### Основные функции
|
||||||
|
|
||||||
|
| Функция | Невериф. | Вериф. | ИГ | Админ | Супер-админ |
|
||||||
|
|---------|----------|--------|----|----|----|
|
||||||
|
| **Главное меню** | ❌ Ограниченное | ✅ Полное | ✅ + ИГ меню | ✅ + Админка | ✅ Всё |
|
||||||
|
| **Экстренные телефоны** | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| **Мастера** | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| **Участковый** | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| **Правила чата** | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| **График отключений** | ❌ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| **Достижения** | ❌ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| **События** | ❌ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| **Платежи ЖКХ** | ❌ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| **Опросы** | ❌ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| **Моя квартира** | ❌ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| **Поблагодарить** | ❌ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| **Помощь** | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Профиль и данные
|
||||||
|
|
||||||
|
| Функция | Невериф. | Вериф. | ИГ | Админ | Супер-админ |
|
||||||
|
|---------|----------|--------|----|----|----|
|
||||||
|
| **Просмотр квартиры** | ❌ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| **Изменить квартиру** | ❌ ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| **Изменить телефон** | ❌ ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| **Статистика ИГ** | ❌ | ❌ | ✅ | ✅ | ✅ |
|
||||||
|
| **Список ИГ** | ❌ | ❌ | ✅ | ✅ | ✅ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Инициативная группа
|
||||||
|
|
||||||
|
| Функция | Невериф. | Вериф. | ИГ | Админ | Супер-админ |
|
||||||
|
|---------|----------|--------|----|----|----|
|
||||||
|
| **Кнопка "ИГ"** | ❌ | ❌ | ✅ | ✅* | ✅* |
|
||||||
|
| **Меню ИГ** | ❌ | ❌ | ✅ | ✅* | ✅* |
|
||||||
|
| **Рассылка ИГ** | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| **Добавить в ИГ** | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| **Удалить из ИГ** | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
|
||||||
|
\* *Если админ добавлен в ИГ*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Админ-панель
|
||||||
|
|
||||||
|
| Функция | Невериф. | Вериф. | ИГ | Админ | Супер-админ |
|
||||||
|
|---------|----------|--------|----|----|----|
|
||||||
|
| **Кнопка "Админка"** | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| **Веб-админка** | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| **Статистика чата** | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| **Шпионы** | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| **Пользователи** | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| **Верификация** | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| **Экспорт JSON** | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| **Рассылка** | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| **Объявления** | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| **Опросы** | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| **События** | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| **Расписания** | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| **Телефоны/Службы** | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| **Настройки** | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Управление пользователями
|
||||||
|
|
||||||
|
| Функция | Невериф. | Вериф. | ИГ | Админ | Супер-админ |
|
||||||
|
|---------|----------|--------|----|----|----|
|
||||||
|
| **Верифицировать** | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| **Снять верификацию** | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| **Забанить** | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| **Разбанить** | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| **Замутить** | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| **Размутить** | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| **Авто-верификация** | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Команды админа
|
||||||
|
|
||||||
|
| Команда | Невериф. | Вериф. | ИГ | Админ | Супер-админ |
|
||||||
|
|---------|----------|--------|----|----|----|
|
||||||
|
| `/admin` | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| `/stats` | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| `/spies` | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| `/export` | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| `/users` | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| `/verify` | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| `/broadcast` | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| `/ban` | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| `/unban` | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| `/mute` | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| `/unmute` | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| `/add_domain` | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| `/domains` | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| `/warnings` | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| `/multicheck` | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| `/toxic_top` | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| `/history_stats` | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Команды ИГ
|
||||||
|
|
||||||
|
| Команда | Невериф. | Вериф. | ИГ | Админ | Супер-админ |
|
||||||
|
|---------|----------|--------|----|----|----|
|
||||||
|
| `/ig_add` | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| `/ig_remove` | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| `/ig_list` | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| `/ig_stats` | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| `/ig_check` | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| `/ig_broadcast` | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Команды профиля
|
||||||
|
|
||||||
|
| Команда | Невериф. | Вериф. | ИГ | Админ | Супер-админ |
|
||||||
|
|---------|----------|--------|----|----|----|
|
||||||
|
| `/setapt` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| `/setphone` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| `/verifications` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| `/my_apartment` | ❌ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 СРАВНЕНИЕ УРОВНЕЙ
|
||||||
|
|
||||||
|
### ❌ Неверифицированный
|
||||||
|
|
||||||
|
**Доступ:**
|
||||||
|
- 📞 Экстренные телефоны
|
||||||
|
- 👷 Мастера
|
||||||
|
- 👮 Участковый
|
||||||
|
- 📜 Правила чата
|
||||||
|
- ❓ Помощь
|
||||||
|
|
||||||
|
**Нет доступа к:**
|
||||||
|
- Графику отключений
|
||||||
|
- Достижениям
|
||||||
|
- Событиям
|
||||||
|
- Платежам
|
||||||
|
- Опросам
|
||||||
|
- Моей квартире
|
||||||
|
- Благодарностям
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✅ Верифицированный
|
||||||
|
|
||||||
|
**Доступ:**
|
||||||
|
- Всё что у невериф. +
|
||||||
|
- 📅 График отключений
|
||||||
|
- 🏆 Достижения
|
||||||
|
- 📅 События
|
||||||
|
- 💰 Платежи
|
||||||
|
- 📋 Опросы
|
||||||
|
- 🔍 Моя квартира
|
||||||
|
- 🙏 Поблагодарить
|
||||||
|
|
||||||
|
**Нет доступа к:**
|
||||||
|
- Меню ИГ
|
||||||
|
- Админке
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🎯 Инициативная группа
|
||||||
|
|
||||||
|
**Доступ:**
|
||||||
|
- Всё что у вериф. +
|
||||||
|
- 🎯 Кнопка "Инициативная группа"
|
||||||
|
- 📊 Статистика ИГ
|
||||||
|
- 📋 Список ИГ
|
||||||
|
- 👤 Мой профиль (редактирование)
|
||||||
|
|
||||||
|
**Нет доступа к:**
|
||||||
|
- Админке
|
||||||
|
- Рассылкам
|
||||||
|
- Управлению пользователями
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ⚙️ Админ
|
||||||
|
|
||||||
|
**Доступ:**
|
||||||
|
- Всё что у ИГ +
|
||||||
|
- ⚙️ Кнопка "Админ-панель"
|
||||||
|
- 📊 Веб-админка
|
||||||
|
- 👥 Управление пользователями
|
||||||
|
- 📢 Рассылки
|
||||||
|
- ✅ Верификация
|
||||||
|
- 🚫 Бан/мут
|
||||||
|
- 📈 Статистика
|
||||||
|
- 📤 Экспорт
|
||||||
|
|
||||||
|
**Нет доступа к:**
|
||||||
|
- Токену бота
|
||||||
|
- Серверу (если не дан)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 👑 Супер-админ (ТЫ)
|
||||||
|
|
||||||
|
**Доступ:**
|
||||||
|
- Всё что у админа +
|
||||||
|
- 🔑 BOT_TOKEN
|
||||||
|
- 🖥️ Доступ к серверу
|
||||||
|
- 📁 Файлы бота
|
||||||
|
- 🗄️ База данных
|
||||||
|
- 📊 Логи
|
||||||
|
- 🔄 Перезапуск бота
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 КАК ИЗМЕНИТЬ СТАТУС
|
||||||
|
|
||||||
|
### Невериф. → Вериф.
|
||||||
|
```
|
||||||
|
1. Жилец отправляет номер квартиры
|
||||||
|
2. Админ подтверждает: /verify @username 45
|
||||||
|
3. Жилец верифицирован ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
### Вериф. → ИГ
|
||||||
|
```
|
||||||
|
1. Админ добавляет: /ig_add <user_id> member
|
||||||
|
2. Жилец в ИГ ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
### Вериф. → Админ
|
||||||
|
```
|
||||||
|
1. В базе: UPDATE users SET is_admin=True WHERE user_id=<id>
|
||||||
|
2. Или в .env: ADMIN_USER_ID=<id>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 ЗАПОМНИ
|
||||||
|
|
||||||
|
| Уровень | Кнопки | Команды | Веб-админка |
|
||||||
|
|---------|--------|---------|-------------|
|
||||||
|
| **Невериф.** | 5 | 3 | ❌ |
|
||||||
|
| **Вериф.** | 12 | 10 | ❌ |
|
||||||
|
| **ИГ** | 14 | 16 | ❌ |
|
||||||
|
| **Админ** | 16 | 30+ | ✅ |
|
||||||
|
| **Супер-админ** | 16 | 30+ | ✅ + сервер |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Спокойной ночи, бро!** 🌙
|
||||||
|
**Бот под надёжным контролем!** 🏠
|
||||||
257
COLOR_MENU_SYSTEM.md
Normal file
257
COLOR_MENU_SYSTEM.md
Normal file
|
|
@ -0,0 +1,257 @@
|
||||||
|
# 🎨 ЦВЕТОВАЯ СИСТЕМА МЕНЮ
|
||||||
|
|
||||||
|
**Обновлено:** 7 марта 2026
|
||||||
|
**Версия:** v1.16
|
||||||
|
**Статус:** ✅ РЕАЛИЗОВАНО
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌈 УРОВНИ И ЦВЕТА
|
||||||
|
|
||||||
|
| Уровень | Emoji | Цвет | Название |
|
||||||
|
|---------|-------|------|----------|
|
||||||
|
| **Неверифицированный** | 🔴 | Красный | НОВИЧОК |
|
||||||
|
| **Верифицированный** | 🟢 | Зелёный | ЖИЛЕЦ |
|
||||||
|
| **Инициативная группа** | 🔵 | Синий | АКТИВ |
|
||||||
|
| **Админ** | 🟡 | Оранжевый | АДМИН |
|
||||||
|
| **Супер-админ** | 🟣 | Фиолетовый | ВЛАДЕЛЕЦ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 КАК ВЫГЛЯДИТ МЕНЮ
|
||||||
|
|
||||||
|
### 🔴 Неверифицированный (Красный)
|
||||||
|
|
||||||
|
```
|
||||||
|
🔴 ДОСТУП ОГРАНИЧЕН
|
||||||
|
|
||||||
|
🔴 НОВИЧОК
|
||||||
|
|
||||||
|
👋 Привет, Александр!
|
||||||
|
|
||||||
|
Я — домовой бот нашего дома.
|
||||||
|
|
||||||
|
❗ Важно: Для полного доступа пройдите верификацию.
|
||||||
|
Отправьте мне номер квартиры (например: 45 или 12А)
|
||||||
|
|
||||||
|
👇 Выберите действие:
|
||||||
|
[🔴 📞 Экстренные телефоны]
|
||||||
|
[🔴 👷 Мастера] [🔴 👮 Участковый]
|
||||||
|
[🔴 📜 Правила чата]
|
||||||
|
[🔴 ❓ Помощь]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🟢 Верифицированный (Зелёный)
|
||||||
|
|
||||||
|
```
|
||||||
|
🟢 ДОСТУП РАЗРЕШЁН
|
||||||
|
|
||||||
|
🟢 ЖИЛЕЦ
|
||||||
|
|
||||||
|
👋 Здравствуйте, Александр!
|
||||||
|
|
||||||
|
✅ Вы верифицированы (кв. 152).
|
||||||
|
Выберите действие:
|
||||||
|
|
||||||
|
[🟢 📞 Экстренные телефоны]
|
||||||
|
[🟢 👷 Мастера]
|
||||||
|
[🟢 📅 График отключений]
|
||||||
|
[🟢 🏆 Достижения] [🟢 📅 События]
|
||||||
|
[🟢 💰 Платежи] [🟢 📋 Опросы]
|
||||||
|
[🟢 🔍 Моя квартира]
|
||||||
|
[🟢 🙏 Поблагодарить]
|
||||||
|
[🟢 ❓ Помощь]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔵 Инициативная группа (Синий)
|
||||||
|
|
||||||
|
```
|
||||||
|
🔵 ИНИЦИАТИВНАЯ ГРУППА
|
||||||
|
|
||||||
|
🔵 АКТИВ
|
||||||
|
|
||||||
|
Вы — участник инициативной группы!
|
||||||
|
|
||||||
|
Ваши возможности:
|
||||||
|
• 📊 Статистика активности
|
||||||
|
• 📋 Список участников
|
||||||
|
• 👤 Мой профиль
|
||||||
|
• 📣 Обращения к администрации
|
||||||
|
• 🗳️ Голосования ИГ
|
||||||
|
|
||||||
|
[🔵 📊 Моя статистика]
|
||||||
|
[🔵 📋 Список ИГ] [🔵 👤 Мой профиль]
|
||||||
|
[🔙 Назад]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🟡 Админ (Оранжевый)
|
||||||
|
|
||||||
|
```
|
||||||
|
🟡 АДМИН-ПАНЕЛЬ
|
||||||
|
|
||||||
|
🟡 АДМИН
|
||||||
|
|
||||||
|
👋 Здравствуйте, Александр!
|
||||||
|
|
||||||
|
✅ Вы верифицированы (кв. 152).
|
||||||
|
Выберите действие:
|
||||||
|
|
||||||
|
[🟡 📞 Экстренные телефоны]
|
||||||
|
[🟡 👷 Мастера]
|
||||||
|
[🟡 📅 График отключений]
|
||||||
|
[🟡 🏆 Достижения] [🟡 📅 События]
|
||||||
|
[🟡 💰 Платежи] [🟡 📋 Опросы]
|
||||||
|
[🟡 🔍 Моя квартира]
|
||||||
|
[🟡 🙏 Поблагодарить]
|
||||||
|
[🟡 ⚙️ Админ-панель]
|
||||||
|
[🟡 ❓ Помощь]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🟣 Супер-админ (Фиолетовый)
|
||||||
|
|
||||||
|
```
|
||||||
|
🟣 УПРАВЛЕНИЕ СИСТЕМОЙ
|
||||||
|
|
||||||
|
🟣 ВЛАДЕЛЕЦ
|
||||||
|
|
||||||
|
👋 Здравствуйте, Александр!
|
||||||
|
|
||||||
|
✅ Вы верифицированы (кв. 152).
|
||||||
|
Выберите действие:
|
||||||
|
|
||||||
|
[🟣 📞 Экстренные телефоны]
|
||||||
|
[🟣 👷 Мастера]
|
||||||
|
[🟣 📅 График отключений]
|
||||||
|
[🟣 🏆 Достижения] [🟣 📅 События]
|
||||||
|
[🟣 💰 Платежи] [🟣 📋 Опросы]
|
||||||
|
[🟣 🔍 Моя квартира]
|
||||||
|
[🟣 🙏 Поблагодарить]
|
||||||
|
[🟣 👑 УПРАВЛЕНИЕ]
|
||||||
|
[🟣 ❓ Помощь]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ ТЕХНИЧЕСКАЯ РЕАЛИЗАЦИЯ
|
||||||
|
|
||||||
|
### Файлы:
|
||||||
|
|
||||||
|
| Файл | Описание |
|
||||||
|
|------|----------|
|
||||||
|
| `utils/ui_styles.py` | Стили и цвета уровней |
|
||||||
|
| `keyboards/inline.py` | Клавиатуры с emoji |
|
||||||
|
| `handlers/users.py` | Обработчики с цветами |
|
||||||
|
| `handlers/initiative_group.py` | Меню ИГ |
|
||||||
|
|
||||||
|
### Функции:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Получить цветной заголовок
|
||||||
|
get_colored_header(text, level)
|
||||||
|
|
||||||
|
# Получить бейдж уровня
|
||||||
|
get_level_badge(level)
|
||||||
|
|
||||||
|
# Определить уровень пользователя
|
||||||
|
get_user_level(user)
|
||||||
|
get_user_level_with_ig(user, is_ig_member)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Цвета (HTML):
|
||||||
|
|
||||||
|
```python
|
||||||
|
LEVEL_COLORS = {
|
||||||
|
'unverified': 'red',
|
||||||
|
'verified': 'green',
|
||||||
|
'ig': 'blue',
|
||||||
|
'admin': 'orange',
|
||||||
|
'superadmin': 'purple',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Emoji:
|
||||||
|
|
||||||
|
```python
|
||||||
|
LEVEL_EMOJI = {
|
||||||
|
'unverified': '🔴',
|
||||||
|
'verified': '🟢',
|
||||||
|
'ig': '🔵',
|
||||||
|
'admin': '🟡',
|
||||||
|
'superadmin': '🟣',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 СРАВНЕНИЕ ДО И ПОСЛЕ
|
||||||
|
|
||||||
|
### ДО (без цветов):
|
||||||
|
|
||||||
|
```
|
||||||
|
[📞 Экстренные телефоны]
|
||||||
|
[👷 Мастера] [👮 Участковый]
|
||||||
|
[📅 График отключений]
|
||||||
|
```
|
||||||
|
|
||||||
|
### ПОСЛЕ (с цветами):
|
||||||
|
|
||||||
|
```
|
||||||
|
[🔴 📞 Экстренные телефоны] ← Неверифицированный
|
||||||
|
[🟢 📞 Экстренные телефоны] ← Верифицированный
|
||||||
|
[🔵 📞 Экстренные телефоны] ← ИГ
|
||||||
|
[🟡 📞 Экстренные телефоны] ← Админ
|
||||||
|
[🟣 📞 Экстренные телефоны] ← Супер-админ
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ ПРЕИМУЩЕСТВА
|
||||||
|
|
||||||
|
1. **Визуальное отличие** — сразу видно уровень доступа
|
||||||
|
2. **Понятность** — новые пользователи видят свой статус
|
||||||
|
3. **Мотивация** — хочется перейти на следующий уровень
|
||||||
|
4. **Красота** — бот выглядит современно
|
||||||
|
5. **UX** — удобная навигация
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 ПРОВЕРКА
|
||||||
|
|
||||||
|
### Пройди тест:
|
||||||
|
|
||||||
|
1. **Напиши /start** — увидишь свой цвет
|
||||||
|
2. **Нажми главное меню** — проверь emoji
|
||||||
|
3. **Посмотри заголовок** — должен быть цветным
|
||||||
|
|
||||||
|
### Ожидаемые результаты:
|
||||||
|
|
||||||
|
| Ты | Цвет | Emoji |
|
||||||
|
|----|------|-------|
|
||||||
|
| Не прошёл верификацию | 🔴 Красный | НОВИЧОК |
|
||||||
|
| Прошёл верификацию | 🟢 Зелёный | ЖИЛЕЦ |
|
||||||
|
| Вступил в ИГ | 🔵 Синий | АКТИВ |
|
||||||
|
| Стал админом | 🟡 Оранжевый | АДМИН |
|
||||||
|
| Ты (владелец) | 🟣 Фиолетовый | ВЛАДЕЛЕЦ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 СЛЕДУЮЩИЕ ФИЧИ (В ПЛАНЕ)
|
||||||
|
|
||||||
|
- [ ] Анимированные emoji при нажатии
|
||||||
|
- [ ] Градиентные заголовки
|
||||||
|
- [ ] Персональные темы (светлая/тёмная)
|
||||||
|
- [ ] Сезонные оформления (зима/лето)
|
||||||
|
- [ ] Праздничные темы (НГ, 8 марта...)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**by MatrixHasYou** 🏠
|
||||||
|
**Цветной бот — красивый бот!** 🎨
|
||||||
237
EDIT_USER_FEATURE.md
Normal file
237
EDIT_USER_FEATURE.md
Normal file
|
|
@ -0,0 +1,237 @@
|
||||||
|
# 📱 УПРАВЛЕНИЕ ПРОФИЛЕМ ЖИЛЬЦА
|
||||||
|
|
||||||
|
**Обновлено:** 7 марта 2026
|
||||||
|
**Версия:** v1.17
|
||||||
|
**Статус:** ✅ РЕАЛИЗОВАНО
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 ЧТО ДОБАВЛЕНО
|
||||||
|
|
||||||
|
1. ✅ **Веб-панель** — кнопка "Редактировать" работает
|
||||||
|
2. ✅ **Telegram бот** — команда `/edit_user` для админа
|
||||||
|
3. ✅ **API endpoint** — `/api/user/{user_id}` для загрузки данных
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 СПОСОБ 1: Telegram бот (БЫСТРО!)
|
||||||
|
|
||||||
|
### Команда для админа:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/edit_user <user_id> [квартира] [телефон]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Примеры:
|
||||||
|
|
||||||
|
**Установить квартиру и телефон:**
|
||||||
|
```bash
|
||||||
|
/edit_user 123456789 45 +79991234567
|
||||||
|
```
|
||||||
|
|
||||||
|
**Только телефон:**
|
||||||
|
```bash
|
||||||
|
/edit_user 123456789 phone:+79991234567
|
||||||
|
```
|
||||||
|
|
||||||
|
**Только квартира:**
|
||||||
|
```bash
|
||||||
|
/edit_user 123456789 apt:45
|
||||||
|
```
|
||||||
|
|
||||||
|
**С префиксами (понятнее):**
|
||||||
|
```bash
|
||||||
|
/edit_user 123456789 apt:45 phone:+79991234567
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Ответ бота:
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ Профиль обновлён!
|
||||||
|
|
||||||
|
👤 Александр Петров
|
||||||
|
🔖 ID: 123456789
|
||||||
|
|
||||||
|
Обновлено:
|
||||||
|
• квартира: 45
|
||||||
|
• телефон: +79991234567
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🖥️ СПОСОБ 2: Веб-панель (УДОБНО!)
|
||||||
|
|
||||||
|
### Пошагово:
|
||||||
|
|
||||||
|
1. **Зайди на** `http://0.0.0.0:8000/users`
|
||||||
|
2. **Найди жильца** в списке (можно через поиск)
|
||||||
|
3. **Нажми** ✏️ **Редактировать**
|
||||||
|
4. **Откроется окно:**
|
||||||
|
```
|
||||||
|
Редактировать пользователя
|
||||||
|
|
||||||
|
Квартира: [45 ]
|
||||||
|
Телефон: [+7999... ]
|
||||||
|
Рейтинг: [0 ]
|
||||||
|
|
||||||
|
[Отмена] [Сохранить]
|
||||||
|
```
|
||||||
|
5. **Впиши данные**
|
||||||
|
6. **Нажми** "Сохранить"
|
||||||
|
7. **Готово!** ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 КАК УЗНАТЬ USER_ID
|
||||||
|
|
||||||
|
### Вариант 1: Веб-админка
|
||||||
|
1. Зайди на `/users`
|
||||||
|
2. Найди жильца
|
||||||
|
3. Скопируй ID из таблицы
|
||||||
|
|
||||||
|
### Вариант 2: Пересылка сообщения
|
||||||
|
1. Перешли сообщение жильца в `@userinfobot`
|
||||||
|
2. Бот покажет ID
|
||||||
|
|
||||||
|
### Вариант 3: Команда в боте
|
||||||
|
```bash
|
||||||
|
/users
|
||||||
|
```
|
||||||
|
(если есть такая команда админа)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 ФОРМАТЫ ДАННЫХ
|
||||||
|
|
||||||
|
### Квартира:
|
||||||
|
- ✅ `45`
|
||||||
|
- ✅ `12А`
|
||||||
|
- ✅ `100-1`
|
||||||
|
- ✅ `45А`
|
||||||
|
|
||||||
|
### Телефон:
|
||||||
|
- ✅ `+79991234567`
|
||||||
|
- ✅ `+7 (999) 123-45-67`
|
||||||
|
- ✅ `+79990001122`
|
||||||
|
|
||||||
|
**Неправильно:**
|
||||||
|
- ❌ `89991234567` (нет +)
|
||||||
|
- ❌ `123` (короткий)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ
|
||||||
|
|
||||||
|
### Пример 1: Знакомый жилец из 45 квартиры
|
||||||
|
|
||||||
|
**Дано:**
|
||||||
|
- Жилец: Иван из 45 квартиры
|
||||||
|
- Телефон: +79991234567
|
||||||
|
- User ID: 123456789
|
||||||
|
|
||||||
|
**Команда:**
|
||||||
|
```bash
|
||||||
|
/edit_user 123456789 45 +79991234567
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Пример 2: Нужно только телефон
|
||||||
|
|
||||||
|
**Дано:**
|
||||||
|
- Жилец знает всех в подъезде
|
||||||
|
- Хочет помочь с телефонами
|
||||||
|
|
||||||
|
**Команда:**
|
||||||
|
```bash
|
||||||
|
/edit_user 123456789 phone:+79991234567
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Пример 3: Массовое обновление
|
||||||
|
|
||||||
|
**Сценарий:** ИГ собрала телефоны подъезда
|
||||||
|
|
||||||
|
**Команды:**
|
||||||
|
```bash
|
||||||
|
/edit_user 111111111 apt:45 phone:+79991111111
|
||||||
|
/edit_user 222222222 apt:46 phone:+79992222222
|
||||||
|
/edit_user 333333333 apt:47 phone:+79993333333
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ ТЕХНИЧЕСКАЯ ЧАСТЬ
|
||||||
|
|
||||||
|
### Изменённые файлы:
|
||||||
|
|
||||||
|
| Файл | Изменения |
|
||||||
|
|------|-----------|
|
||||||
|
| `web/app.py` | GET `/api/user/{user_id}` |
|
||||||
|
| `handlers/users.py` | Команда `/edit_user` |
|
||||||
|
| `web/templates/users.html` | Модалка редактирования |
|
||||||
|
|
||||||
|
### API endpoint:
|
||||||
|
|
||||||
|
**GET** `/api/user/{user_id}`
|
||||||
|
|
||||||
|
**Ответ:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"user": {
|
||||||
|
"id": 123456789,
|
||||||
|
"username": "ivan",
|
||||||
|
"first_name": "Иван",
|
||||||
|
"last_name": "Петров",
|
||||||
|
"apartment": "45",
|
||||||
|
"phone": "+79991234567",
|
||||||
|
"verified": true,
|
||||||
|
"rating": 150,
|
||||||
|
"message_count": 500,
|
||||||
|
"is_banned": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ ПРОВЕРКА
|
||||||
|
|
||||||
|
### 1. Веб-панель:
|
||||||
|
- [ ] Зайти на `/users`
|
||||||
|
- [ ] Нажать ✏️ на любом пользователе
|
||||||
|
- [ ] Изменить квартиру/телефон
|
||||||
|
- [ ] Сохранить
|
||||||
|
- [ ] Проверить что обновилось
|
||||||
|
|
||||||
|
### 2. Telegram бот:
|
||||||
|
- [ ] Написать `/edit_user 123456789 45 +79991234567`
|
||||||
|
- [ ] Получить подтверждение
|
||||||
|
- [ ] Проверить через "Моя квартира"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 СОВЕТЫ
|
||||||
|
|
||||||
|
1. **Инициативная группа** может собирать телефоны
|
||||||
|
2. **Админ** быстро вносит через `/edit_user`
|
||||||
|
3. **Веб-панель** удобна для массового редактирования
|
||||||
|
4. **Проверяй** данные перед сохранением
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆕 СЛЕДУЮЩИЕ ФИЧИ (В ПЛАНЕ)
|
||||||
|
|
||||||
|
- [ ] Массовое редактирование (выбрать несколько)
|
||||||
|
- [ ] Экспорт телефонов (CSV)
|
||||||
|
- [ ] Импорт телефонов (CSV)
|
||||||
|
- [ ] Поиск по телефону
|
||||||
|
- [ ] История изменений профиля
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**by MatrixHasYou** 🏠
|
||||||
|
**Теперь редактирование профилей доступно везде!** 📱🖥️
|
||||||
206
FORUM_TOPICS_SETUP.md
Normal file
206
FORUM_TOPICS_SETUP.md
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
# 🏢 TELEGRAM FORUM TOPICS — НАСТРОЙКА БОТА
|
||||||
|
|
||||||
|
**Обновлено:** 4 марта 2026
|
||||||
|
**Статус:** ✅ НАСТРОЕНО
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 ЧТО СДЕЛАНО
|
||||||
|
|
||||||
|
Бот теперь поддерживает **Telegram Forum Topics** — отдельные ветки внутри чата!
|
||||||
|
|
||||||
|
### 10 тем настроено:
|
||||||
|
|
||||||
|
| № | Тема | ID | Куда отправляет бот |
|
||||||
|
|---|------|-----|---------------------|
|
||||||
|
| 1 | **Общий чат** | `1` | Рассылки, объявления, уведомления |
|
||||||
|
| 2 | **Важные объявления** | `6258` | Объявления жильцов (`/ad`) |
|
||||||
|
| 3 | **Ссылки** | `6265` | — |
|
||||||
|
| 4 | **Мемы** | `6262` | — |
|
||||||
|
| 5 | **Для автовладельцев** | `6266` | — |
|
||||||
|
| 6 | **Потеряшки** | `6263` | Потерянные вещи (`/lost`) |
|
||||||
|
| 7 | **Собрания собственников** | `6261` | События, собрания (`/events`) |
|
||||||
|
| 8 | **Вывоз КГМ** | `6264` | Напоминания о вывозе мусора |
|
||||||
|
| 9 | **Переписка с УО** | `6260` | — |
|
||||||
|
| 10 | **Важные телефоны** | `6259` | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ КОНФИГУРАЦИЯ
|
||||||
|
|
||||||
|
### .env
|
||||||
|
```ini
|
||||||
|
# Telegram Forum Topics
|
||||||
|
FORUM_ENABLED=true
|
||||||
|
TOPIC_GENERAL=1
|
||||||
|
TOPIC_ADS=6258
|
||||||
|
TOPIC_LINKS=6265
|
||||||
|
TOPIC_MEMES=6262
|
||||||
|
TOPIC_AUTO=6266
|
||||||
|
TOPIC_LOSTFOUND=6263
|
||||||
|
TOPIC_MEETINGS=6261
|
||||||
|
TOPIC_KGM=6264
|
||||||
|
TOPIC_UO=6260
|
||||||
|
TOPIC_PHONES=6259
|
||||||
|
```
|
||||||
|
|
||||||
|
### config.py
|
||||||
|
```python
|
||||||
|
FORUM_TOPICS = {
|
||||||
|
'general': 1,
|
||||||
|
'ads': 6258,
|
||||||
|
'links': 6265,
|
||||||
|
'memes': 6262,
|
||||||
|
'auto': 6266,
|
||||||
|
'lostfound': 6263,
|
||||||
|
'meetings': 6261,
|
||||||
|
'kgm': 6264,
|
||||||
|
'uo': 6260,
|
||||||
|
'phones': 6259,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 КАК БОТ ИСПОЛЬЗУЕТ ТЕМЫ
|
||||||
|
|
||||||
|
### Объявления (`/ad`)
|
||||||
|
```python
|
||||||
|
# Отправка в тему "Важные объявления"
|
||||||
|
await bot.send_message(
|
||||||
|
chat_id=ADMIN_CHAT_ID,
|
||||||
|
message_thread_id=get_topic_id('ads'), # 6258
|
||||||
|
text="Объявление..."
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Рассылки от админа
|
||||||
|
```python
|
||||||
|
# Отправка в "Общий чат"
|
||||||
|
await bot.send_message(
|
||||||
|
chat_id=ADMIN_CHAT_ID,
|
||||||
|
message_thread_id=get_topic_id('general'), # 1
|
||||||
|
text="Рассылка..."
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### События (`/events`)
|
||||||
|
```python
|
||||||
|
# Отправка в "Собрания собственников"
|
||||||
|
await bot.send_message(
|
||||||
|
chat_id=ADMIN_CHAT_ID,
|
||||||
|
message_thread_id=get_topic_id('meetings'), # 6261
|
||||||
|
text="Событие..."
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 ЧТО КУДА ОТПРАВЛЯЕТСЯ
|
||||||
|
|
||||||
|
| Тип контента | Тема | ID |
|
||||||
|
|-------------|------|-----|
|
||||||
|
| **Объявления жильцов** (`/ad`) | Важные объявления | 6258 |
|
||||||
|
| **Рассылки админа** | Общий чат | 1 |
|
||||||
|
| **Напоминания ЖКХ** | Общий чат | 1 |
|
||||||
|
| **Отключения (вода, свет)** | Общий чат | 1 |
|
||||||
|
| **События дома** | Собрания собственников | 6261 |
|
||||||
|
| **Потерянные вещи** | Потеряшки | 6263 |
|
||||||
|
| **Вывоз КГМ** | Вывоз КГМ | 6264 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 ДОБАВЛЕНИЕ НОВЫХ ТЕМ
|
||||||
|
|
||||||
|
1. **Добавь в `.env`:**
|
||||||
|
```ini
|
||||||
|
TOPIC_NEW=12345
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Обнови `config.py`:**
|
||||||
|
```python
|
||||||
|
FORUM_TOPICS = {
|
||||||
|
...
|
||||||
|
'new': TOPIC_NEW,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Используй в коде:**
|
||||||
|
```python
|
||||||
|
await bot.send_message(
|
||||||
|
chat_id=config.ADMIN_CHAT_ID,
|
||||||
|
message_thread_id=config.get_topic_id('new'),
|
||||||
|
text="..."
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Перезапусти бота:**
|
||||||
|
```bash
|
||||||
|
cd /home/matrixhasyou/domovoy_bot
|
||||||
|
./stop.sh && ./run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 УМНЫЙ АНАЛИЗАТОР ТЕМ
|
||||||
|
|
||||||
|
Бот автоматически определяет тему по тексту сообщения:
|
||||||
|
|
||||||
|
| Ключевые слова | Тема |
|
||||||
|
|---------------|------|
|
||||||
|
| "продам", "куплю", "отдам" | Важные объявления |
|
||||||
|
| "потерял", "нашёл", "ключи" | Потеряшки |
|
||||||
|
| "собрание", "голосование" | Собрания собственников |
|
||||||
|
| "мусор", "вывоз", "КГМ" | Вывоз КГМ |
|
||||||
|
| "сантехник", "электрик" | Важные телефоны |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ ПРОВЕРКА РАБОТЫ
|
||||||
|
|
||||||
|
1. **Создай объявление:**
|
||||||
|
```
|
||||||
|
/ad
|
||||||
|
```
|
||||||
|
→ Должно появиться в теме "Важные объявления"
|
||||||
|
|
||||||
|
2. **Отправь рассылку:**
|
||||||
|
```
|
||||||
|
/broadcast
|
||||||
|
```
|
||||||
|
→ Должно появиться в "Общем чате"
|
||||||
|
|
||||||
|
3. **Создай событие:**
|
||||||
|
```
|
||||||
|
/event_add
|
||||||
|
```
|
||||||
|
→ Должно появиться в "Собрания собственников"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ ОТЛАДКА
|
||||||
|
|
||||||
|
### Бот отправляет не в ту тему:
|
||||||
|
```bash
|
||||||
|
# Проверь .env
|
||||||
|
cat .env | grep TOPIC_
|
||||||
|
|
||||||
|
# Проверь config.py
|
||||||
|
python -c "from config import FORUM_TOPICS; print(FORUM_TOPICS)"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ошибка "Invalid message_thread_id":
|
||||||
|
```bash
|
||||||
|
# Проверь что тема существует
|
||||||
|
# Открой тему в Telegram и проверь URL
|
||||||
|
```
|
||||||
|
|
||||||
|
### Бот не видит сообщения из тем:
|
||||||
|
```bash
|
||||||
|
# Проверь права бота в каждой теме
|
||||||
|
# Бот должен быть админом со доступом к сообщениям
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**by MatrixHasYou** 🏠
|
||||||
235
INITIATIVE_GROUP_GUIDE.md
Normal file
235
INITIATIVE_GROUP_GUIDE.md
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
# 🎯 ИНИЦИАТИВНАЯ ГРУППА — ИНСТРУКЦИЯ
|
||||||
|
|
||||||
|
**Обновлено:** 5 марта 2026
|
||||||
|
**Статус:** ✅ РЕАЛИЗОВАНО
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 ЧТО ТАКОЕ ИНИЦИАТИВНАЯ ГРУППА (ИГ)?
|
||||||
|
|
||||||
|
**Инициативная группа** — это активные жильцы, которые помогают управлять домом.
|
||||||
|
|
||||||
|
### Уровни доступа:
|
||||||
|
|
||||||
|
| Уровень | Доступ |
|
||||||
|
|---------|--------|
|
||||||
|
| **Неверифицированный** | Только телефоны + правила |
|
||||||
|
| **Верифицированный** | Все функции бота |
|
||||||
|
| **Инициативная группа** | Всё + меню ИГ |
|
||||||
|
| **Админ** | Всё + админ-панель |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ НАЗНАЧЕНИЕ В ИГ
|
||||||
|
|
||||||
|
### Можешь ли назначать НЕзависимо от верификации?
|
||||||
|
|
||||||
|
**ДА!** ✅ Верификация **НЕ требуется** для вступления в ИГ!
|
||||||
|
|
||||||
|
### Как назначить в ИГ:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Добавить пользователя в ИГ
|
||||||
|
/ig_add <user_id> [роль]
|
||||||
|
|
||||||
|
# Роли:
|
||||||
|
# - member (обычный участник)
|
||||||
|
# - admin (админ ИГ)
|
||||||
|
# - coordinator (координатор)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Примеры:**
|
||||||
|
```bash
|
||||||
|
/ig_add 197957361 admin
|
||||||
|
/ig_add 123456789 member
|
||||||
|
/ig_add 987654321 coordinator
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 МЕНЮ ИНИЦИАТИВНОЙ ГРУППЫ
|
||||||
|
|
||||||
|
Кнопка **"🎯 Инициативная группа"** появляется в главном меню только у участников ИГ!
|
||||||
|
|
||||||
|
### Что видит участник ИГ:
|
||||||
|
|
||||||
|
```
|
||||||
|
🎯 Инициативная группа
|
||||||
|
|
||||||
|
Вы — участник инициативной группы!
|
||||||
|
|
||||||
|
Ваши возможности:
|
||||||
|
• 📊 Статистика активности
|
||||||
|
• 📋 Список участников
|
||||||
|
• 📣 Обращения к администрации
|
||||||
|
• 🗳️ Голосования ИГ
|
||||||
|
|
||||||
|
[📊 Моя статистика]
|
||||||
|
[📋 Список ИГ]
|
||||||
|
[🔙 Назад]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Моя статистика:
|
||||||
|
```
|
||||||
|
📊 Ваша статистика в ИГ
|
||||||
|
|
||||||
|
👤 Александр MatrixHasYou
|
||||||
|
🏠 Квартира: 152
|
||||||
|
|
||||||
|
🎭 Роль: admin
|
||||||
|
📅 В группе с: 05.03.2026
|
||||||
|
|
||||||
|
📊 Ваша активность:
|
||||||
|
• Сообщений: 150
|
||||||
|
• Рейтинг: 250
|
||||||
|
• Уровень: Активист
|
||||||
|
```
|
||||||
|
|
||||||
|
### Список ИГ:
|
||||||
|
```
|
||||||
|
📋 Участники инициативной группы
|
||||||
|
|
||||||
|
👑 Админы
|
||||||
|
• Александр MatrixHasYou (кв. 152)
|
||||||
|
|
||||||
|
🎯 Координаторы
|
||||||
|
• Иван Петров (кв. 45)
|
||||||
|
|
||||||
|
👥 Участники
|
||||||
|
• Мария Сидорова (кв. 12А)
|
||||||
|
• Пётр Васильев (кв. 78)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 КОМАНДЫ АДМИНА ДЛЯ ИГ
|
||||||
|
|
||||||
|
### Добавить в ИГ:
|
||||||
|
```bash
|
||||||
|
/ig_add <user_id> [роль]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Удалить из ИГ:
|
||||||
|
```bash
|
||||||
|
/ig_remove <user_id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Список участников ИГ:
|
||||||
|
```bash
|
||||||
|
/ig_list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Статистика ИГ:
|
||||||
|
```bash
|
||||||
|
/ig_stats
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проверить участника:
|
||||||
|
```bash
|
||||||
|
/ig_check <user_id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Рассылка только ИГ:
|
||||||
|
```bash
|
||||||
|
/ig_broadcast
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ
|
||||||
|
|
||||||
|
### 1. Назначить активного жильца в ИГ:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Узнай ID пользователя (через веб-админку или /users)
|
||||||
|
/ig_add 123456789 member
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Назначить координатора по подъезду:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/ig_add 987654321 coordinator
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Посмотреть кто в ИГ:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/ig_list
|
||||||
|
```
|
||||||
|
|
||||||
|
Вывод:
|
||||||
|
```
|
||||||
|
📋 Участники инициативной группы
|
||||||
|
|
||||||
|
Всего: 5
|
||||||
|
Активных: 4
|
||||||
|
|
||||||
|
• member: 3
|
||||||
|
• coordinator: 1
|
||||||
|
• admin: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Рассылка только ИГ:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/ig_broadcast
|
||||||
|
```
|
||||||
|
|
||||||
|
Бот попросит отправить текст → отправь сообщение → оно уйдёт только участникам ИГ!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 КАК НАЗНАЧИТЬ ЖИЛЬЦА В ИГ (ПОШАГОВО)
|
||||||
|
|
||||||
|
### Шаг 1: Найди ID жильца
|
||||||
|
|
||||||
|
**Способ 1:** Веб-админка → Пользователи → найди жильца → скопируй ID
|
||||||
|
|
||||||
|
**Способ 2:** Перешли сообщение жильца в `@userinfobot` → получишь ID
|
||||||
|
|
||||||
|
**Способ 3:** Команда `/users` в боте → список пользователей
|
||||||
|
|
||||||
|
### Шаг 2: Назначь в ИГ
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/ig_add 123456789 member
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 3: Проверь
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/ig_check 123456789
|
||||||
|
```
|
||||||
|
|
||||||
|
Ответ:
|
||||||
|
```
|
||||||
|
✅ Пользователь является участником ИГ
|
||||||
|
Роль: member
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 4: Готово!
|
||||||
|
|
||||||
|
Теперь у жильца появилась кнопка **"🎯 Инициативная группа"** в меню!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ ВАЖНО
|
||||||
|
|
||||||
|
1. **Верификация НЕ обязательна** — можешь назначать кого угодно
|
||||||
|
2. **ИГ ≠ Админ** — у ИГ нет прав админки, только доп. функции
|
||||||
|
3. **Можно удалить** — `/ig_remove` деактивирует участника
|
||||||
|
4. **Роли имеют значение** — для статистики и группировки
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆕 СЛЕДУЮЩИЕ ФИЧИ (В ПЛАНЕ)
|
||||||
|
|
||||||
|
- [ ] Голосования ИГ
|
||||||
|
- [ ] Обращения к администрации
|
||||||
|
- [ ] Задачи для ИГ
|
||||||
|
- [ ] Статистика активности ИГ
|
||||||
|
- [ ] Чат ИГ (отдельная тема)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**by MatrixHasYou** 🏠
|
||||||
214
PROFILE_FEATURE.md
Normal file
214
PROFILE_FEATURE.md
Normal file
|
|
@ -0,0 +1,214 @@
|
||||||
|
# 📞 ПРОФИЛЬ ЖИЛЬЦА — ИНСТРУКЦИЯ
|
||||||
|
|
||||||
|
**Обновлено:** 5 марта 2026
|
||||||
|
**Статус:** ✅ РЕАЛИЗОВАНО
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 ЧТО ДОБАВЛЕНО
|
||||||
|
|
||||||
|
### 1. ✅ Изменение квартиры и телефона
|
||||||
|
|
||||||
|
**Доступно ВСЕМ пользователям** (не только ИГ!)
|
||||||
|
|
||||||
|
### 2. ✅ Команды для редактирования
|
||||||
|
|
||||||
|
| Команда | Описание |
|
||||||
|
|---------|----------|
|
||||||
|
| `/setapt 45` | Изменить квартиру |
|
||||||
|
| `/setapt 12А` | Квартира с буквой |
|
||||||
|
| `/setphone +79991234567` | Изменить телефон |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 ИНИЦИАТИВНАЯ ГРУППА — МЕНЮ
|
||||||
|
|
||||||
|
Кнопка **"🎯 Инициативная группа"** → меню:
|
||||||
|
|
||||||
|
```
|
||||||
|
🎯 Инициативная группа
|
||||||
|
|
||||||
|
Вы — участник инициативной группы!
|
||||||
|
|
||||||
|
Ваши возможности:
|
||||||
|
• 📊 Статистика активности
|
||||||
|
• 📋 Список участников
|
||||||
|
• 👤 Мой профиль
|
||||||
|
• 📣 Обращения к администрации
|
||||||
|
• 🗳️ Голосования ИГ
|
||||||
|
|
||||||
|
[📊 Моя статистика]
|
||||||
|
[📋 Список ИГ] [👤 Мой профиль]
|
||||||
|
[🔙 Назад]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Мой профиль (кнопка):
|
||||||
|
|
||||||
|
```
|
||||||
|
👤 Ваш профиль
|
||||||
|
|
||||||
|
🏠 Квартира: 152
|
||||||
|
📞 Телефон: не указан
|
||||||
|
|
||||||
|
Что хотите изменить?
|
||||||
|
|
||||||
|
[🏠 Изменить квартиру]
|
||||||
|
[📞 Изменить телефон]
|
||||||
|
[🔙 Назад]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 КАК ИЗМЕНИТЬ ПРОФИЛЬ
|
||||||
|
|
||||||
|
### Способ 1: Через кнопки (ИГ)
|
||||||
|
|
||||||
|
1. Нажми **"🎯 Инициативная группа"**
|
||||||
|
2. Нажми **"👤 Мой профиль"**
|
||||||
|
3. Выбери **"🏠 Изменить квартиру"** или **"📞 Изменить телефон"**
|
||||||
|
4. Бот покажет команду
|
||||||
|
5. Отправь команду с новым значением
|
||||||
|
|
||||||
|
### Способ 2: Командами (все)
|
||||||
|
|
||||||
|
**Изменить квартиру:**
|
||||||
|
```bash
|
||||||
|
/setapt 45
|
||||||
|
```
|
||||||
|
|
||||||
|
**Изменить телефон:**
|
||||||
|
```bash
|
||||||
|
/setphone +79991234567
|
||||||
|
```
|
||||||
|
|
||||||
|
### Способ 3: Через "Моя квартира"
|
||||||
|
|
||||||
|
1. Нажми **"🔍 Моя квартира"** в главном меню
|
||||||
|
2. Увидишь текущие данные
|
||||||
|
3. Бот подскажет команды для изменения
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ ВАЛИДАЦИЯ
|
||||||
|
|
||||||
|
### Квартира:
|
||||||
|
- ✅ `45`
|
||||||
|
- ✅ `12А`
|
||||||
|
- ✅ `100-1`
|
||||||
|
- ✅ `45А`
|
||||||
|
- ❌ `abc`
|
||||||
|
- ❌ `` (пусто)
|
||||||
|
|
||||||
|
### Телефон:
|
||||||
|
- ✅ `+79991234567`
|
||||||
|
- ✅ `+7 (999) 123-45-67`
|
||||||
|
- ❌ `89991234567` (нет +)
|
||||||
|
- ❌ `123` (короткий)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 КОНФИДЕНЦИАЛЬНОСТЬ
|
||||||
|
|
||||||
|
### Кто видит телефон?
|
||||||
|
|
||||||
|
| Кто | Видит телефон |
|
||||||
|
|-----|---------------|
|
||||||
|
| **Админ** | ✅ Да (в веб-админке) |
|
||||||
|
| **Инициативная группа** | ❌ Нет (только свой) |
|
||||||
|
| **Обычные жильцы** | ❌ Нет |
|
||||||
|
|
||||||
|
### Где показывается телефон?
|
||||||
|
|
||||||
|
- ✅ В профиле пользователя (только владелец)
|
||||||
|
- ✅ В веб-админке (для админов)
|
||||||
|
- ❌ В общем чате — **НИКОГДА**
|
||||||
|
- ❌ В списке ИГ — **НЕТ**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 ВСЁ ЛИЧНОЕ ОБЩЕНИЕ — В ЛИЧКЕ!
|
||||||
|
|
||||||
|
### ✅ Бот общается только в личных сообщениях!
|
||||||
|
|
||||||
|
**Никакого завала в общем чате!**
|
||||||
|
|
||||||
|
- Верификация → в личке
|
||||||
|
- Изменение профиля → в личке
|
||||||
|
- Команды → в личке
|
||||||
|
- Уведомления → в личке
|
||||||
|
|
||||||
|
### В общий чат бот отправляет:
|
||||||
|
|
||||||
|
- 📢 Объявления (по команде админа)
|
||||||
|
- 📅 Напоминания о отключениях
|
||||||
|
- 💰 Напоминания о платежах
|
||||||
|
- 🏆 Объявления жильцов (`/ad`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ
|
||||||
|
|
||||||
|
### Пример 1: Изменить квартиру
|
||||||
|
|
||||||
|
```
|
||||||
|
User: /setapt 45
|
||||||
|
Bot: ✅ Квартира обновлена!
|
||||||
|
Ваша квартира: 45
|
||||||
|
```
|
||||||
|
|
||||||
|
### Пример 2: Изменить телефон
|
||||||
|
|
||||||
|
```
|
||||||
|
User: /setphone +79991234567
|
||||||
|
Bot: ✅ Телефон обновлён!
|
||||||
|
Ваш телефон: +79991234567
|
||||||
|
```
|
||||||
|
|
||||||
|
### Пример 3: Посмотреть профиль (ИГ)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. User нажимает: 🎯 Инициативная группа
|
||||||
|
2. User нажимает: 👤 Мой профиль
|
||||||
|
|
||||||
|
Bot: 👤 Ваш профиль
|
||||||
|
|
||||||
|
🏠 Квартира: 152
|
||||||
|
📞 Телефон: не указан
|
||||||
|
|
||||||
|
Что хотите изменить?
|
||||||
|
|
||||||
|
[🏠 Изменить квартиру]
|
||||||
|
[📞 Изменить телефон]
|
||||||
|
[🔙 Назад]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 СТРУКТУРА БАЗЫ ДАННЫХ
|
||||||
|
|
||||||
|
### Таблица `users`:
|
||||||
|
|
||||||
|
| Поле | Тип | Описание |
|
||||||
|
|------|-----|----------|
|
||||||
|
| `user_id` | BigInteger | ID Telegram |
|
||||||
|
| `username` | String | @username |
|
||||||
|
| `first_name` | String | Имя |
|
||||||
|
| `last_name` | String | Фамилия |
|
||||||
|
| `apartment` | String | **Квартира** (изменяется) |
|
||||||
|
| `phone` | String | **Телефон** (изменяется) |
|
||||||
|
| `verified` | Boolean | Верифицирован |
|
||||||
|
| ... | ... | ... |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆕 СЛЕДУЮЩИЕ ФИЧИ (В ПЛАНЕ)
|
||||||
|
|
||||||
|
- [ ] Экспорт телефонов ИГ (для админа)
|
||||||
|
- [ ] Поиск жильца по телефону (админ)
|
||||||
|
- [ ] Приватность телефона (скрыть/показать)
|
||||||
|
- [ ] Фото профиля в ИГ
|
||||||
|
- [ ] Должность в ИГ (координатор, старший подъезда)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**by MatrixHasYou** 🏠
|
||||||
185
UX_UPDATE_v1.15.md
Normal file
185
UX_UPDATE_v1.15.md
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
# 🎨 ОБНОВЛЕНИЕ МЕНЮ И UX — v1.15
|
||||||
|
|
||||||
|
**Дата:** 5 марта 2026
|
||||||
|
**Статус:** ✅ РЕАЛИЗОВАНО
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 ЧТО ИЗМЕНИЛОСЬ
|
||||||
|
|
||||||
|
### 1. ✅ Убрано дублирование "Участкового"
|
||||||
|
|
||||||
|
**Было:**
|
||||||
|
- Кнопка "👮 Участковый" в главном меню
|
||||||
|
- Кнопка "👮 Полиция" в меню телефонов
|
||||||
|
|
||||||
|
**Стало:**
|
||||||
|
- Кнопка "👮 Полиция" только в меню телефонов (без дублирования)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. ✅ 3 уровня пользователей
|
||||||
|
|
||||||
|
| Уровень | Статус | Доступ |
|
||||||
|
|---------|--------|--------|
|
||||||
|
| **Админ** | `is_admin=True` | Все функции + админка |
|
||||||
|
| **Инициативная группа** | Член IG | Все функции + IG меню |
|
||||||
|
| **Жильцы** | Все остальные | Зависит от верификации |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. ✅ Верифицированные vs Неверифицированные
|
||||||
|
|
||||||
|
#### **Неверифицированные** (ограниченный доступ):
|
||||||
|
- 📞 Экстренные телефоны
|
||||||
|
- 👷 Мастера
|
||||||
|
- 👮 Участковый
|
||||||
|
- 📜 Правила чата
|
||||||
|
- ❓ Помощь
|
||||||
|
|
||||||
|
#### **Верифицированные** (полный доступ):
|
||||||
|
- Всё выше +
|
||||||
|
- 📅 График отключений
|
||||||
|
- 🏆 Достижения
|
||||||
|
- 📅 События
|
||||||
|
- 💰 Платежи
|
||||||
|
- 📋 Опросы
|
||||||
|
- 🔍 Моя квартира
|
||||||
|
- 🙏 Поблагодарить
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. ✅ Обновлённая "Помощь"
|
||||||
|
|
||||||
|
**Новый текст для новичков:**
|
||||||
|
|
||||||
|
```
|
||||||
|
🏠 Домовой бот — помощь
|
||||||
|
|
||||||
|
🆕 Вы впервые здесь?
|
||||||
|
|
||||||
|
1️⃣ Пройдите верификацию — это просто!
|
||||||
|
• Отправьте боту номер вашей квартиры
|
||||||
|
• Например: 45 или 12А
|
||||||
|
• После проверки вы получите полный доступ
|
||||||
|
|
||||||
|
2️⃣ Что будет после верификации?
|
||||||
|
• 📅 График отключений воды и света
|
||||||
|
• 📢 Объявления и новости дома
|
||||||
|
• 💰 Напоминания о платежах ЖКХ
|
||||||
|
• 🏆 Рейтинг и достижения
|
||||||
|
• 📋 Участие в опросах
|
||||||
|
• 📅 События и собрания
|
||||||
|
|
||||||
|
📞 Телефоны служб
|
||||||
|
Нажмите "Экстренные телефоны" чтобы увидеть:
|
||||||
|
• 🚨 Аварийные службы (02, 03, 04, 112)
|
||||||
|
• 🔧 ЖКХ, сантехники, электрики
|
||||||
|
• 👮 Участковый полиция
|
||||||
|
|
||||||
|
❓ Нужна помощь?
|
||||||
|
Напишите администратору чата
|
||||||
|
|
||||||
|
🔐 Конфиденциальность
|
||||||
|
Бот видит только ваши сообщения в этом чате.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 КАК ЭТО РАБОТАЕТ
|
||||||
|
|
||||||
|
### /start — главное меню
|
||||||
|
|
||||||
|
**Неверифицированный:**
|
||||||
|
```
|
||||||
|
👋 Здравствуйте, Александр!
|
||||||
|
|
||||||
|
⏳ Вы ещё не верифицированы.
|
||||||
|
|
||||||
|
📝 Что делать?
|
||||||
|
Отправьте мне номер вашей квартиры текстом:
|
||||||
|
45 или 12А
|
||||||
|
|
||||||
|
После проверки вы получите доступ ко всем функциям!
|
||||||
|
|
||||||
|
[📞 Экстренные телефоны]
|
||||||
|
[👷 Мастера] [👮 Участковый]
|
||||||
|
[📜 Правила чата]
|
||||||
|
[❓ Помощь]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Верифицированный:**
|
||||||
|
```
|
||||||
|
👋 Здравствуйте, Александр!
|
||||||
|
|
||||||
|
✅ Вы верифицированы (кв. 152).
|
||||||
|
Выберите действие:
|
||||||
|
|
||||||
|
[📞 Экстренные телефоны]
|
||||||
|
[👷 Мастера]
|
||||||
|
[📅 График отключений]
|
||||||
|
[🏆 Достижения] [📅 События]
|
||||||
|
[💰 Платежи] [📋 Опросы]
|
||||||
|
[🔍 Моя квартира]
|
||||||
|
[🙏 Поблагодарить]
|
||||||
|
[❓ Помощь]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Админ/ИГ:**
|
||||||
|
```
|
||||||
|
+ кнопка "⚙️ Админ-панель" или "🎯 Инициативная группа"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 ИЗМЕНЁННЫЕ ФАЙЛЫ
|
||||||
|
|
||||||
|
| Файл | Изменения |
|
||||||
|
|------|-----------|
|
||||||
|
| `keyboards/inline.py` | `get_main_menu(verified, is_ig, is_admin)` |
|
||||||
|
| `keyboards/inline.py` | `get_unverified_menu()` — новое |
|
||||||
|
| `handlers/users.py` | Обновлён `/start` |
|
||||||
|
| `handlers/users.py` | Обновлена `/help` |
|
||||||
|
| `handlers/users.py` | Добавлена `/rules` |
|
||||||
|
| `handlers/users.py` | Обновлён callback `main_menu` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 ПРОВЕРКА
|
||||||
|
|
||||||
|
### 1. Новый пользователь:
|
||||||
|
```
|
||||||
|
/start
|
||||||
|
```
|
||||||
|
→ Видит ограниченное меню + правила
|
||||||
|
|
||||||
|
### 2. Верифицированный:
|
||||||
|
```
|
||||||
|
/start
|
||||||
|
```
|
||||||
|
→ Видит полное меню
|
||||||
|
|
||||||
|
### 3. Админ:
|
||||||
|
```
|
||||||
|
/start
|
||||||
|
```
|
||||||
|
→ Видит кнопку "⚙️ Админ-панель"
|
||||||
|
|
||||||
|
### 4. Помощь:
|
||||||
|
```
|
||||||
|
/help или кнопка "❓ Помощь"
|
||||||
|
```
|
||||||
|
→ Подробная инструкция для новичков
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 СЛЕДУЮЩИЕ ШАГИ
|
||||||
|
|
||||||
|
1. **Добавить кнопку "Участковый"** в меню телефонов ✅
|
||||||
|
2. **Проверить отображение** на мобильных устройствах
|
||||||
|
3. **Протестировать** на новом пользователе
|
||||||
|
4. **Обновить инструкцию** для жильцов
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**by MatrixHasYou** 🏠
|
||||||
153
WEB_USER_MANAGEMENT.md
Normal file
153
WEB_USER_MANAGEMENT.md
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
# 🖥️ ВЕБ-ПАНЕЛЬ: УПРАВЛЕНИЕ ПОЛЬЗОВАТЕЛЯМИ
|
||||||
|
|
||||||
|
**Обновлено:** 8 марта 2026
|
||||||
|
**Версия:** v1.18
|
||||||
|
**Статус:** ✅ РЕАЛИЗОВАНО
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 ЧТО ДОБАВЛЕНО
|
||||||
|
|
||||||
|
Теперь в веб-панели есть **полное управление пользователями**:
|
||||||
|
|
||||||
|
| Кнопка | Действие |
|
||||||
|
|--------|----------|
|
||||||
|
| ✏️ | Редактировать (квартира, телефон, рейтинг) |
|
||||||
|
| ✅ / ❌ | Верифицировать / Отозвать |
|
||||||
|
| 🚫 / 🔓 | Забанить / Разбанить |
|
||||||
|
| 🖤 | **Назначить админом** (НОВОЕ!) |
|
||||||
|
| 🩵 | **Добавить в ИГ** (НОВОЕ!) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 КАК ВЫГЛЯДИТ ТАБЛИЦА
|
||||||
|
|
||||||
|
```
|
||||||
|
ID | Имя | Username | Квартира | Телефон | Вериф. | Бан | Рейтинг | ДЕЙСТВИЯ
|
||||||
|
--------|---------------|----------|----------|---------------|--------|------|---------|----------
|
||||||
|
197957361 | Александр | @admin | 152 | +7999... | ✅ Да | ❌ Нет | 500 | ✏️ 🖤 🩵
|
||||||
|
123456789 | Иван | @ivan | 45 | +7999... | ❌ Нет | ❌ Нет | 50 | ✏️ ✅ 🖤 🩵
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔘 НОВЫЕ КНОПКИ
|
||||||
|
|
||||||
|
### 1. 🖤 Назначить админом
|
||||||
|
|
||||||
|
**Нажми** → Подтверждение → Готово!
|
||||||
|
|
||||||
|
**Что делает:**
|
||||||
|
- ✅ Устанавливает `is_admin=True`
|
||||||
|
- ✅ Пользователь получает доступ к админке
|
||||||
|
- ✅ Видит кнопку "⚙️ Админ-панель"
|
||||||
|
|
||||||
|
**Обратная кнопка:** 🖤 (контур) — Снять админа
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 🩵 Добавить в ИГ
|
||||||
|
|
||||||
|
**Нажми** → Подтверждение → Готово!
|
||||||
|
|
||||||
|
**Что делает:**
|
||||||
|
- ✅ Добавляет в `InitiativeGroup` с ролью `member`
|
||||||
|
- ✅ Пользователь видит кнопку "🎯 Инициативная группа"
|
||||||
|
- ✅ Имеет доступ к функциям ИГ
|
||||||
|
|
||||||
|
**Обратная кнопка:** 🩵 (контур) — Удалить из ИГ
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ
|
||||||
|
|
||||||
|
### Сценарий 1: Назначить админа
|
||||||
|
|
||||||
|
1. Зайди на `/users`
|
||||||
|
2. Найди пользователя (поиск по имени/квартире)
|
||||||
|
3. Нажми 🖤 **Назначить админом**
|
||||||
|
4. Подтверди
|
||||||
|
5. ✅ Готово!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Сценарий 2: Добавить в ИГ
|
||||||
|
|
||||||
|
1. Зайди на `/users`
|
||||||
|
2. Найди пользователя
|
||||||
|
3. Нажми 🩵 **Добавить в ИГ**
|
||||||
|
4. Подтверди
|
||||||
|
5. ✅ Готово!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Сценарий 3: Полный цикл
|
||||||
|
|
||||||
|
**Дано:** Новый жилец, активист, хочет помочь
|
||||||
|
|
||||||
|
**Делаем:**
|
||||||
|
1. ✏️ Редактировать → Квартира: 45, Телефон: +7999...
|
||||||
|
2. ✅ Верифицировать
|
||||||
|
3. 🩵 Добавить в ИГ
|
||||||
|
4. (Опционально) 🖤 Назначить админом
|
||||||
|
|
||||||
|
**Результат:**
|
||||||
|
- ✅ Верифицирован
|
||||||
|
- ✅ В Инициативной Группе
|
||||||
|
- ✅ Имеет доступ ко всем функциям
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 API ENDPOINTS
|
||||||
|
|
||||||
|
| Endpoint | Метод | Описание |
|
||||||
|
|----------|-------|----------|
|
||||||
|
| `/api/user/{id}/make_admin` | POST | Назначить админом |
|
||||||
|
| `/api/user/{id}/remove_admin` | POST | Снять админа |
|
||||||
|
| `/api/user/{id}/add_to_ig` | POST | Добавить в ИГ |
|
||||||
|
| `/api/user/{id}/remove_from_ig` | POST | Удалить из ИГ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ ПРОВЕРКА
|
||||||
|
|
||||||
|
### 1. Зайди в веб-админку:
|
||||||
|
```
|
||||||
|
http://0.0.0.0:8000/users
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Проверь кнопки:
|
||||||
|
- [ ] 🖤 Назначить админом
|
||||||
|
- [ ] 🩵 Добавить в ИГ
|
||||||
|
- [ ] ✏️ Редактировать
|
||||||
|
- [ ] ✅ Верифицировать
|
||||||
|
|
||||||
|
### 3. Протестируй:
|
||||||
|
- Назначь админа → проверь в боте
|
||||||
|
- Добавь в ИГ → проверь в боте
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 СОВЕТЫ
|
||||||
|
|
||||||
|
1. **Админов назначай осторожно** — это доступ ко всем функциям
|
||||||
|
2. **ИГ — только активные** — те кто реально помогает
|
||||||
|
3. **Проверяй перед назначением** — убедись что человек надёжный
|
||||||
|
4. **Веди список** — запиши кого и когда назначил
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 СРАВНЕНИЕ: ВЕБ vs TELEGRAM
|
||||||
|
|
||||||
|
| Действие | Веб-панель | Telegram |
|
||||||
|
|----------|------------|----------|
|
||||||
|
| Назначить админа | ✅ 1 клик | ❌ Нет команды |
|
||||||
|
| Добавить в ИГ | ✅ 1 клик | ✅ `/ig_add` |
|
||||||
|
| Редактировать | ✅ Модалка | ✅ `/edit_user` |
|
||||||
|
| Верифицировать | ✅ 1 клик | ✅ `/verify` |
|
||||||
|
| Массовое | ❌ По одному | ❌ По одному |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**by MatrixHasYou** 🏠
|
||||||
|
**Полное управление пользователями в веб-панели!** 🖥️✅
|
||||||
43
config.py
43
config.py
|
|
@ -41,6 +41,33 @@ WEB_ADMIN_PASSWORD = os.getenv('WEB_ADMIN_PASSWORD', 'admin')
|
||||||
WEB_ADMIN_HOST = os.getenv('WEB_ADMIN_HOST', '0.0.0.0')
|
WEB_ADMIN_HOST = os.getenv('WEB_ADMIN_HOST', '0.0.0.0')
|
||||||
WEB_ADMIN_PORT = int(os.getenv('WEB_ADMIN_PORT', '8000'))
|
WEB_ADMIN_PORT = int(os.getenv('WEB_ADMIN_PORT', '8000'))
|
||||||
|
|
||||||
|
# ===== TELEGRAM FORUM TOPICS =====
|
||||||
|
FORUM_ENABLED = os.getenv('FORUM_ENABLED', 'false').lower() == 'true'
|
||||||
|
TOPIC_GENERAL = int(os.getenv('TOPIC_GENERAL', '1'))
|
||||||
|
TOPIC_ADS = int(os.getenv('TOPIC_ADS', '1'))
|
||||||
|
TOPIC_LINKS = int(os.getenv('TOPIC_LINKS', '1'))
|
||||||
|
TOPIC_MEMES = int(os.getenv('TOPIC_MEMES', '1'))
|
||||||
|
TOPIC_AUTO = int(os.getenv('TOPIC_AUTO', '1'))
|
||||||
|
TOPIC_LOSTFOUND = int(os.getenv('TOPIC_LOSTFOUND', '1'))
|
||||||
|
TOPIC_MEETINGS = int(os.getenv('TOPIC_MEETINGS', '1'))
|
||||||
|
TOPIC_KGM = int(os.getenv('TOPIC_KGM', '1'))
|
||||||
|
TOPIC_UO = int(os.getenv('TOPIC_UO', '1'))
|
||||||
|
TOPIC_PHONES = int(os.getenv('TOPIC_PHONES', '1'))
|
||||||
|
|
||||||
|
# Словарь всех тем
|
||||||
|
FORUM_TOPICS = {
|
||||||
|
'general': TOPIC_GENERAL,
|
||||||
|
'ads': TOPIC_ADS,
|
||||||
|
'links': TOPIC_LINKS,
|
||||||
|
'memes': TOPIC_MEMES,
|
||||||
|
'auto': TOPIC_AUTO,
|
||||||
|
'lostfound': TOPIC_LOSTFOUND,
|
||||||
|
'meetings': TOPIC_MEETINGS,
|
||||||
|
'kgm': TOPIC_KGM,
|
||||||
|
'uo': TOPIC_UO,
|
||||||
|
'phones': TOPIC_PHONES,
|
||||||
|
}
|
||||||
|
|
||||||
# Проверка критичных настроек
|
# Проверка критичных настроек
|
||||||
if not BOT_TOKEN:
|
if not BOT_TOKEN:
|
||||||
raise ValueError('BOT_TOKEN не настроен! Проверьте .env файл')
|
raise ValueError('BOT_TOKEN не настроен! Проверьте .env файл')
|
||||||
|
|
@ -80,3 +107,19 @@ def get_admin_ids():
|
||||||
def is_admin(user_id: int) -> bool:
|
def is_admin(user_id: int) -> bool:
|
||||||
"""Проверка, является ли пользователь админом"""
|
"""Проверка, является ли пользователь админом"""
|
||||||
return user_id in get_admin_ids()
|
return user_id in get_admin_ids()
|
||||||
|
|
||||||
|
|
||||||
|
def get_topic_id(topic_type: str) -> int:
|
||||||
|
"""
|
||||||
|
Получить ID темы Forum по типу контента
|
||||||
|
|
||||||
|
Args:
|
||||||
|
topic_type: тип контента ('ads', 'lostfound', 'meetings', etc.)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
message_thread_id для отправки в тему
|
||||||
|
"""
|
||||||
|
if not FORUM_ENABLED:
|
||||||
|
return None # Отправлять в общую тему (без message_thread_id)
|
||||||
|
|
||||||
|
return FORUM_TOPICS.get(topic_type.lower(), TOPIC_GENERAL)
|
||||||
|
|
|
||||||
|
|
@ -364,12 +364,12 @@ async def process_broadcast_message(message: Message):
|
||||||
|
|
||||||
async def send_broadcast_text(bot, text: str):
|
async def send_broadcast_text(bot, text: str):
|
||||||
"""Рассылка текста"""
|
"""Рассылка текста"""
|
||||||
# Отправляем в чат дома
|
# Отправляем в чат дома в тему "Важные объявления"
|
||||||
try:
|
try:
|
||||||
await bot.send_message(
|
await bot.send_message(
|
||||||
config.ADMIN_CHAT_ID,
|
chat_id=config.ADMIN_CHAT_ID,
|
||||||
f'📢 <b>Объявление от администрации</b>\n\n'
|
message_thread_id=config.get_topic_id('general'), # В общую тему
|
||||||
f'{text}',
|
text=f'📢 <b>Объявление от администрации</b>\n\n{text}',
|
||||||
parse_mode='HTML'
|
parse_mode='HTML'
|
||||||
)
|
)
|
||||||
logger.info(f'Рассылка отправлена в чат {config.ADMIN_CHAT_ID}')
|
logger.info(f'Рассылка отправлена в чат {config.ADMIN_CHAT_ID}')
|
||||||
|
|
@ -411,11 +411,12 @@ async def send_broadcast_text(bot, text: str):
|
||||||
|
|
||||||
async def send_broadcast_with_photo(bot, photo_id: str, caption: str):
|
async def send_broadcast_with_photo(bot, photo_id: str, caption: str):
|
||||||
"""Рассылка картинки с подписью"""
|
"""Рассылка картинки с подписью"""
|
||||||
# Отправляем в чат дома
|
# Отправляем в чат дома в тему "Важные объявления"
|
||||||
try:
|
try:
|
||||||
await bot.send_photo(
|
await bot.send_photo(
|
||||||
config.ADMIN_CHAT_ID,
|
chat_id=config.ADMIN_CHAT_ID,
|
||||||
photo_id,
|
message_thread_id=config.get_topic_id('general'), # В общую тему
|
||||||
|
photo=photo_id,
|
||||||
caption=f'📢 <b>Объявление от администрации</b>\n\n{caption}',
|
caption=f'📢 <b>Объявление от администрации</b>\n\n{caption}',
|
||||||
parse_mode='HTML'
|
parse_mode='HTML'
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,7 @@ async def process_ads_phone(message: Message, state: FSMContext):
|
||||||
|
|
||||||
# Сохраняем объявление в БД
|
# Сохраняем объявление в БД
|
||||||
data = await state.get_data()
|
data = await state.get_data()
|
||||||
expires_at = datetime.utcnow() + timedelta(days=7)
|
expires_at = datetime.utcnow() + timedelta(days=14) # 14 дней вместо 7
|
||||||
|
|
||||||
async with AsyncSessionLocal() as session:
|
async with AsyncSessionLocal() as session:
|
||||||
ad = Ad(
|
ad = Ad(
|
||||||
|
|
@ -228,11 +228,12 @@ async def process_ads_phone(message: Message, state: FSMContext):
|
||||||
ad_text += f'\n⏳ Действует до: {expires_at.strftime("%d.%m.%Y")}\n'
|
ad_text += f'\n⏳ Действует до: {expires_at.strftime("%d.%m.%Y")}\n'
|
||||||
ad_text += f'🔖 ID: {ad.ad_id}'
|
ad_text += f'🔖 ID: {ad.ad_id}'
|
||||||
|
|
||||||
# Отправляем в чат
|
# Отправляем в чат в тему "Важные объявления"
|
||||||
try:
|
try:
|
||||||
sent_message = await message.bot.send_message(
|
sent_message = await message.bot.send_message(
|
||||||
config.ADMIN_CHAT_ID,
|
chat_id=config.ADMIN_CHAT_ID,
|
||||||
ad_text,
|
message_thread_id=config.get_topic_id('ads'), # Тема "Важные объявления"
|
||||||
|
text=ad_text,
|
||||||
parse_mode='HTML'
|
parse_mode='HTML'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ Initiative Group — хендлеры для работы с инициатив
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from aiogram import Router, F
|
from aiogram import Router, F
|
||||||
from aiogram.types import Message
|
from aiogram.types import Message, CallbackQuery
|
||||||
from aiogram.filters import Command, StateFilter
|
from aiogram.filters import Command, StateFilter
|
||||||
from aiogram.fsm.state import State, StatesGroup
|
from aiogram.fsm.state import State, StatesGroup
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
|
|
@ -19,10 +19,108 @@ logger = logging.getLogger(__name__)
|
||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# МЕНЮ ИНИЦИАТИВНОЙ ГРУППЫ (CALLBACK)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@router.callback_query(F.data == 'ig_menu')
|
||||||
|
async def cb_ig_menu(callback: CallbackQuery):
|
||||||
|
"""Меню инициативной группы"""
|
||||||
|
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||||
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
from utils.ui_styles import get_colored_header, get_level_badge
|
||||||
|
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
user_id = callback.from_user.id
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
ig_service = InitiativeGroupService(session)
|
||||||
|
is_member = await ig_service.is_member(user_id)
|
||||||
|
|
||||||
|
if is_member:
|
||||||
|
header = get_colored_header('ИНИЦИАТИВНАЯ ГРУППА', 'ig')
|
||||||
|
|
||||||
|
text = f'{header}\n\n'
|
||||||
|
text += f'{get_level_badge("ig")}\n\n'
|
||||||
|
text += 'Вы — участник инициативной группы!\n\n'
|
||||||
|
text += '<b>Ваши возможности:</b>\n'
|
||||||
|
text += '• 📊 Статистика активности\n'
|
||||||
|
text += '• 📋 Список участников\n'
|
||||||
|
text += '• 👤 Мой профиль\n'
|
||||||
|
text += '• 📣 Рассылка для ИГ\n'
|
||||||
|
text += '• 📢 Обращения к администрации\n'
|
||||||
|
text += '• 🗳️ Голосования ИГ\n\n'
|
||||||
|
text += '<i>Больше функций в разработке...</i>'
|
||||||
|
|
||||||
|
builder = InlineKeyboardBuilder()
|
||||||
|
builder.row(
|
||||||
|
InlineKeyboardButton(text='🔵 📊 Моя статистика', callback_data='ig_my_stats'),
|
||||||
|
)
|
||||||
|
builder.row(
|
||||||
|
InlineKeyboardButton(text='🔵 📋 Список ИГ', callback_data='ig_list_members'),
|
||||||
|
InlineKeyboardButton(text='🔵 👤 Мой профиль', callback_data='ig_my_profile'),
|
||||||
|
)
|
||||||
|
builder.row(
|
||||||
|
InlineKeyboardButton(text='🔵 📣 Рассылка ИГ', callback_data='ig_broadcast'),
|
||||||
|
)
|
||||||
|
builder.row(
|
||||||
|
InlineKeyboardButton(text='🔙 Назад', callback_data='main_menu'),
|
||||||
|
)
|
||||||
|
|
||||||
|
await callback.message.answer(
|
||||||
|
text,
|
||||||
|
reply_markup=builder.as_markup(),
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await callback.message.answer(
|
||||||
|
'ℹ️ <b>Инициативная группа</b>\n\n'
|
||||||
|
'Вы не являетесь участником ИГ.\n\n'
|
||||||
|
'Инициативная группа — это активные жильцы,\n'
|
||||||
|
'которые помогают управлять домом.\n\n'
|
||||||
|
'Чтобы вступить, обратитесь к администратору.',
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# FSM для добавления в ИГ
|
# FSM для добавления в ИГ
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
|
class IGState(StatesGroup):
|
||||||
|
waiting_for_notes = State()
|
||||||
|
waiting_for_broadcast = State()
|
||||||
|
waiting_for_broadcast_file = State()
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# РАССЫЛКА ДЛЯ ИГ
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@router.callback_query(F.data == 'ig_broadcast')
|
||||||
|
async def cb_ig_broadcast(callback: CallbackQuery):
|
||||||
|
"""Рассылка для ИГ"""
|
||||||
|
from aiogram.types import InputFile
|
||||||
|
|
||||||
|
await callback.answer()
|
||||||
|
await callback.message.answer(
|
||||||
|
'📣 <b>Рассылка для Инициативной Группы</b>\n\n'
|
||||||
|
'Отправьте текст сообщения:\n\n'
|
||||||
|
'<i>Или прикрепите файл (PDF, фото и т.д.)</i>\n\n'
|
||||||
|
'Отправьте "отмена" для выхода.',
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
await callback.from_user.id
|
||||||
|
await callback.message.bot.get_updates()
|
||||||
|
await callback.message.answer('👇 Отправьте текст или файл:')
|
||||||
|
# Проще через команду
|
||||||
|
await callback.message.answer(
|
||||||
|
'ℹ️ <b>Для отправки рассылки используйте команду:</b>\n\n'
|
||||||
|
'<code>/ig_broadcast</code>\n\n'
|
||||||
|
'Бот попросит ввести текст и файл.',
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
|
||||||
class IGState(StatesGroup):
|
class IGState(StatesGroup):
|
||||||
waiting_for_notes = State()
|
waiting_for_notes = State()
|
||||||
|
|
||||||
|
|
@ -99,12 +197,170 @@ async def process_ig_notes(message: Message, state: FSMContext):
|
||||||
if notes and notes.lower() != 'пропустить':
|
if notes and notes.lower() != 'пропустить':
|
||||||
async with AsyncSessionLocal() as session:
|
async with AsyncSessionLocal() as session:
|
||||||
ig_service = InitiativeGroupService(session)
|
ig_service = InitiativeGroupService(session)
|
||||||
await ig_service.update_role(user_id, 'member') # Можно расширить для сохранения notes
|
await ig_service.update_role(user_id, 'member')
|
||||||
|
|
||||||
await message.answer('✅ Заметка сохранена')
|
await message.answer('✅ Заметка сохранена')
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# СТАТИСТИКА УЧАСТНИКА ИГ
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@router.callback_query(F.data == 'ig_my_stats')
|
||||||
|
async def cb_ig_my_stats(callback: CallbackQuery):
|
||||||
|
"""Статистика участника ИГ"""
|
||||||
|
from database.models import User
|
||||||
|
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
user_id = callback.from_user.id
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
ig_service = InitiativeGroupService(session)
|
||||||
|
member_info = await ig_service.get_member(user_id)
|
||||||
|
user = await session.get(User, user_id)
|
||||||
|
|
||||||
|
if member_info and user:
|
||||||
|
text = '📊 <b>Ваша статистика в ИГ</b>\n\n'
|
||||||
|
text += f'👤 <b>{user.full_name or user.username}</b>\n'
|
||||||
|
text += f'🏠 Квартира: {user.apartment or "не указана"}\n\n'
|
||||||
|
text += f'🎭 <b>Роль:</b> {member_info["role"]}\n'
|
||||||
|
text += f'📅 <b>В группе с:</b> {member_info["joined_at"].strftime("%d.%m.%Y")}\n\n'
|
||||||
|
text += f'📊 <b>Ваша активность:</b>\n'
|
||||||
|
text += f'• Сообщений: {user.message_count}\n'
|
||||||
|
text += f'• Рейтинг: {user.rating}\n'
|
||||||
|
text += f'• Уровень: {user.level}\n'
|
||||||
|
|
||||||
|
await callback.message.answer(text, parse_mode='HTML')
|
||||||
|
else:
|
||||||
|
await callback.message.answer('❌ Вы не являетесь участником ИГ')
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# СПИСОК УЧАСТНИКОВ ИГ
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@router.callback_query(F.data == 'ig_list_members')
|
||||||
|
async def cb_ig_list_members(callback: CallbackQuery):
|
||||||
|
"""Список участников ИГ"""
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
ig_service = InitiativeGroupService(session)
|
||||||
|
members = await ig_service.get_members(active_only=True)
|
||||||
|
|
||||||
|
if members:
|
||||||
|
text = '📋 <b>Участники инициативной группы</b>\n\n'
|
||||||
|
|
||||||
|
# Группируем по ролям
|
||||||
|
by_role = {}
|
||||||
|
for m in members:
|
||||||
|
role = m['role']
|
||||||
|
if role not in by_role:
|
||||||
|
by_role[role] = []
|
||||||
|
by_role[role].append(m)
|
||||||
|
|
||||||
|
role_names = {
|
||||||
|
'admin': '👑 Админы',
|
||||||
|
'coordinator': '🎯 Координаторы',
|
||||||
|
'member': '👥 Участники'
|
||||||
|
}
|
||||||
|
|
||||||
|
for role, role_members in by_role.items():
|
||||||
|
text += f'\n<b>{role_names.get(role, role)}</b>\n'
|
||||||
|
for m in role_members[:10]: # Макс 10 на роль
|
||||||
|
apt = f' (кв. {m["apartment"]})' if m.get('apartment') else ''
|
||||||
|
text += f'• {m["name"]}{apt}\n'
|
||||||
|
|
||||||
|
if len(members) > 30:
|
||||||
|
text += f'\n... и ещё {len(members) - 30} участников'
|
||||||
|
|
||||||
|
await callback.message.answer(text, parse_mode='HTML')
|
||||||
|
else:
|
||||||
|
await callback.message.answer('ℹ️ В ИГ пока нет участников')
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# МОЙ ПРОФИЛЬ (РЕДАКТИРОВАНИЕ)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@router.callback_query(F.data == 'ig_my_profile')
|
||||||
|
async def cb_ig_my_profile(callback: CallbackQuery):
|
||||||
|
"""Редактирование профиля участника ИГ"""
|
||||||
|
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||||
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
from database.models import User
|
||||||
|
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
user_id = callback.from_user.id
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
user = await session.get(User, user_id)
|
||||||
|
|
||||||
|
if user:
|
||||||
|
phone_text = f'📞 <b>Телефон:</b> <code>{user.phone or "не указан"}</code>\n'
|
||||||
|
apt_text = f'🏠 <b>Квартира:</b> <code>{user.apartment or "не указана"}</code>\n'
|
||||||
|
|
||||||
|
text = '👤 <b>Ваш профиль</b>\n\n'
|
||||||
|
text += apt_text
|
||||||
|
text += phone_text
|
||||||
|
text += '\n<b>Что хотите изменить?</b>'
|
||||||
|
|
||||||
|
builder = InlineKeyboardBuilder()
|
||||||
|
builder.row(
|
||||||
|
InlineKeyboardButton(text='🏠 Изменить квартиру', callback_data='ig_edit_apt'),
|
||||||
|
)
|
||||||
|
builder.row(
|
||||||
|
InlineKeyboardButton(text='📞 Изменить телефон', callback_data='ig_edit_phone'),
|
||||||
|
)
|
||||||
|
builder.row(
|
||||||
|
InlineKeyboardButton(text='🔙 Назад', callback_data='ig_menu'),
|
||||||
|
)
|
||||||
|
|
||||||
|
await callback.message.answer(
|
||||||
|
text,
|
||||||
|
reply_markup=builder.as_markup(),
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await callback.message.answer('❌ Вы не найдены в базе')
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# ИЗМЕНИТЬ КВАРТИРУ
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@router.callback_query(F.data == 'ig_edit_apt')
|
||||||
|
async def cb_ig_edit_apt(callback: CallbackQuery):
|
||||||
|
"""Изменение квартиры"""
|
||||||
|
await callback.answer()
|
||||||
|
await callback.message.answer(
|
||||||
|
'🏠 <b>Изменение квартиры</b>\n\n'
|
||||||
|
'ℹ️ <b>Используйте команду:</b>\n\n'
|
||||||
|
'<code>/setapt 45</code>\n\n'
|
||||||
|
'Где 45 — ваш номер квартиры.\n\n'
|
||||||
|
'Или нажмите "Назад".',
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# ИЗМЕНИТЬ ТЕЛЕФОН
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@router.callback_query(F.data == 'ig_edit_phone')
|
||||||
|
async def cb_ig_edit_phone(callback: CallbackQuery):
|
||||||
|
"""Изменение телефона"""
|
||||||
|
await callback.answer()
|
||||||
|
await callback.message.answer(
|
||||||
|
'📞 <b>Изменение телефона</b>\n\n'
|
||||||
|
'ℹ️ <b>Используйте команду:</b>\n\n'
|
||||||
|
'<code>/setphone +79991234567</code>\n\n'
|
||||||
|
'Или нажмите "Назад".',
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# УДАЛЕНИЕ ИЗ ИГ
|
# УДАЛЕНИЕ ИЗ ИГ
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
|
||||||
|
|
@ -372,11 +372,12 @@ async def send_payment_reminders(bot):
|
||||||
|
|
||||||
text += '\n_Оплатить можно через Госуслуги, Сбербанк или Тинькофф_'
|
text += '\n_Оплатить можно через Госуслуги, Сбербанк или Тинькофф_'
|
||||||
|
|
||||||
# Отправляем в чат
|
# Отправляем в чат в тему "Важные объявления"
|
||||||
try:
|
try:
|
||||||
await bot.send_message(
|
await bot.send_message(
|
||||||
config.ADMIN_CHAT_ID,
|
chat_id=config.ADMIN_CHAT_ID,
|
||||||
text,
|
message_thread_id=config.get_topic_id('general'), # В общую тему
|
||||||
|
text=text,
|
||||||
parse_mode='HTML',
|
parse_mode='HTML',
|
||||||
disable_web_page_preview=True
|
disable_web_page_preview=True
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -348,14 +348,15 @@ async def check_and_send_reminders(bot):
|
||||||
schedules = list(result.scalars().all())
|
schedules = list(result.scalars().all())
|
||||||
|
|
||||||
for schedule in schedules:
|
for schedule in schedules:
|
||||||
# Отправляем напоминание в чат
|
# Отправляем напоминание в чат в тему "Важные объявления"
|
||||||
try:
|
try:
|
||||||
emoji = schedule.get_type_emoji()
|
emoji = schedule.get_type_emoji()
|
||||||
duration = (schedule.end_time - schedule.start_time).total_seconds() / 3600
|
duration = (schedule.end_time - schedule.start_time).total_seconds() / 3600
|
||||||
|
|
||||||
await bot.send_message(
|
await bot.send_message(
|
||||||
config.ADMIN_CHAT_ID,
|
chat_id=config.ADMIN_CHAT_ID,
|
||||||
f'⏰ <b>Напоминание: отключение через 1 час!</b>\n\n'
|
message_thread_id=config.get_topic_id('general'), # В общую тему
|
||||||
|
text=f'⏰ <b>Напоминание: отключение через 1 час!</b>\n\n'
|
||||||
f'{emoji} <b>{schedule.title}</b>\n\n'
|
f'{emoji} <b>{schedule.title}</b>\n\n'
|
||||||
f'📅 Начало: {schedule.start_time.strftime("%d.%m.%Y %H:%M")}\n'
|
f'📅 Начало: {schedule.start_time.strftime("%d.%m.%Y %H:%M")}\n'
|
||||||
f'📅 Окончание: {schedule.end_time.strftime("%d.%m.%Y %H:%M")}\n'
|
f'📅 Окончание: {schedule.end_time.strftime("%d.%m.%Y %H:%M")}\n'
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ router = Router()
|
||||||
@router.message(CommandStart())
|
@router.message(CommandStart())
|
||||||
async def cmd_start(message: Message):
|
async def cmd_start(message: Message):
|
||||||
"""Команда /start (ТОЛЬКО В ЛИЧКЕ!)"""
|
"""Команда /start (ТОЛЬКО В ЛИЧКЕ!)"""
|
||||||
|
from utils.ui_styles import get_level_badge
|
||||||
|
from aiogram.types import ReplyKeyboardMarkup, KeyboardButton
|
||||||
|
|
||||||
# Проверяем что это личка
|
# Проверяем что это личка
|
||||||
if message.chat.type != 'private':
|
if message.chat.type != 'private':
|
||||||
await message.answer(
|
await message.answer(
|
||||||
|
|
@ -51,62 +54,213 @@ async def cmd_start(message: Message):
|
||||||
session.add(user)
|
session.add(user)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
welcome_text = (
|
# 🔴 НОВОЕ СООБЩЕНИЕ С КНОПКАМИ (БЕЗ <font>!)
|
||||||
f'🏠 <b>Добро пожаловать, {message.from_user.first_name}!</b>\n\n'
|
welcome_text = f'''🔴 <b>ДОБРО ПОЖАЛОВАТЬ</b>
|
||||||
f'Я — домовой бот нашего дома.\n\n'
|
|
||||||
f'❗ <b>Важно:</b> Для полного доступа пройдите верификацию.\n'
|
👋 <b>Привет, {message.from_user.first_name}!</b>
|
||||||
f'Отправьте мне номер вашей квартиры (например: <code>45</code> или <code>12А</code>)'
|
|
||||||
|
Я — домовой бот нашего дома.
|
||||||
|
|
||||||
|
🏠 <b>Что я умею:</b>
|
||||||
|
• 📞 Телефоны экстренных служб
|
||||||
|
• 👷 Проверенные мастера
|
||||||
|
• 📅 График отключений
|
||||||
|
• 💰 Напоминания о платежах
|
||||||
|
• 🏆 Рейтинг и достижения
|
||||||
|
• 📋 Опросы и голосования
|
||||||
|
|
||||||
|
⚠️ <b>ВАЖНО:</b>
|
||||||
|
Для полного доступа пройдите верификацию!
|
||||||
|
|
||||||
|
📝 <b>Как пройти верификацию:</b>
|
||||||
|
|
||||||
|
<b>Шаг 1:</b> Отправьте номер квартиры
|
||||||
|
Примеры: <code>45</code>, <code>12А</code>, <code>100-1</code>
|
||||||
|
|
||||||
|
<b>Шаг 2:</b> (опционально) Отправьте телефон
|
||||||
|
Пример: <code>+79991234567</code>
|
||||||
|
|
||||||
|
<b>Шаг 3:</b> Дождитесь подтверждения админа
|
||||||
|
|
||||||
|
После верификации откроются все функции! 🎉'''
|
||||||
|
|
||||||
|
# Кнопки для быстрого старта
|
||||||
|
kb = ReplyKeyboardMarkup(
|
||||||
|
keyboard=[
|
||||||
|
[KeyboardButton(text='📞 Экстренные телефоны')],
|
||||||
|
[KeyboardButton(text='📜 Правила чата'), KeyboardButton(text='❓ Помощь')],
|
||||||
|
],
|
||||||
|
resize_keyboard=True
|
||||||
)
|
)
|
||||||
await message.answer(welcome_text, parse_mode='HTML')
|
|
||||||
|
await message.answer(welcome_text, parse_mode='HTML', reply_markup=kb)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Обновляем имя
|
# Обновляем имя
|
||||||
user.username = message.from_user.username
|
user.username = message.from_user.username
|
||||||
user.first_name = message.from_user.first_name
|
user.first_name = message.from_user.first_name
|
||||||
user.last_name = message.from_user.last_name
|
user.last_name = message.from_user.last_name
|
||||||
await session.commit()
|
|
||||||
|
# Проверяем статус
|
||||||
|
is_admin = user.is_admin or (user_id == config.ADMIN_USER_ID)
|
||||||
|
is_superadmin = (user_id == config.ADMIN_USER_ID)
|
||||||
|
|
||||||
if user.verified:
|
if user.verified:
|
||||||
|
# Проверяем членство в инициативной группе
|
||||||
|
from database.models import InitiativeGroup
|
||||||
|
ig_stmt = select(InitiativeGroup).where(
|
||||||
|
InitiativeGroup.user_id == user_id,
|
||||||
|
InitiativeGroup.is_active == True
|
||||||
|
)
|
||||||
|
ig_result = await session.execute(ig_stmt)
|
||||||
|
ig_member = ig_result.scalar_one_or_none()
|
||||||
|
is_ig = ig_member is not None
|
||||||
|
|
||||||
|
# Определяем уровень
|
||||||
|
if is_superadmin:
|
||||||
|
level = 'superadmin'
|
||||||
|
header = '🟣 <b>УПРАВЛЕНИЕ СИСТЕМОЙ</b>'
|
||||||
|
elif is_admin:
|
||||||
|
level = 'admin'
|
||||||
|
header = '🟡 <b>АДМИН-ПАНЕЛЬ</b>'
|
||||||
|
elif is_ig:
|
||||||
|
level = 'ig'
|
||||||
|
header = '🔵 <b>ИНИЦИАТИВНАЯ ГРУППА</b>'
|
||||||
|
else:
|
||||||
|
level = 'verified'
|
||||||
|
header = '🟢 <b>ДОСТУП РАЗРЕШЁН</b>'
|
||||||
|
|
||||||
await message.answer(
|
await message.answer(
|
||||||
|
f'{header}\n\n'
|
||||||
f'👋 Здравствуйте, {message.from_user.first_name}!\n\n'
|
f'👋 Здравствуйте, {message.from_user.first_name}!\n\n'
|
||||||
f'Вы верифицированы (кв. {user.apartment}).\n'
|
f'{get_level_badge(level)}\n\n'
|
||||||
|
f'✅ Вы верифицированы (кв. {user.apartment}).\n'
|
||||||
f'Выберите действие:',
|
f'Выберите действие:',
|
||||||
reply_markup=get_main_menu(),
|
reply_markup=get_main_menu(verified=True, is_ig=is_ig, is_admin=is_admin, is_superadmin=is_superadmin),
|
||||||
parse_mode='HTML'
|
parse_mode='HTML'
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
# Неверифицированный — подробная инструкция (БЕЗ <font>!)
|
||||||
|
verify_text = f'''🔴 <b>ТРЕБУЕТСЯ ВЕРИФИКАЦИЯ</b>
|
||||||
|
|
||||||
|
👋 Здравствуйте, {message.from_user.first_name}!
|
||||||
|
|
||||||
|
{get_level_badge("unverified")}
|
||||||
|
|
||||||
|
⏳ <b>Вы ещё не верифицированы</b>
|
||||||
|
|
||||||
|
📝 <b>Что делать?</b>
|
||||||
|
|
||||||
|
<b>1️⃣ Отправьте номер квартиры</b>
|
||||||
|
Примеры:
|
||||||
|
<code>45</code>
|
||||||
|
<code>12А</code>
|
||||||
|
<code>100-1</code>
|
||||||
|
|
||||||
|
<b>2️⃣ (опционально) Телефон</b>
|
||||||
|
Пример:
|
||||||
|
<code>+79991234567</code>
|
||||||
|
|
||||||
|
<b>3️⃣ Дождитесь подтверждения</b>
|
||||||
|
Админ проверит и подтвердит.
|
||||||
|
|
||||||
|
✅ После верификации получите доступ ко всем функциям!
|
||||||
|
|
||||||
|
💡 <b>Нужна помощь?</b>
|
||||||
|
Напишите администратору чата.'''
|
||||||
|
|
||||||
await message.answer(
|
await message.answer(
|
||||||
f'👋 Здравствуйте, {message.from_user.first_name}!\n\n'
|
verify_text,
|
||||||
f'⏳ Вы ещё не верифицированы.\n'
|
reply_markup=get_unverified_menu(),
|
||||||
f'Отправьте номер квартиры для верификации.',
|
|
||||||
parse_mode='HTML'
|
parse_mode='HTML'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
@router.message(Command('помощь'))
|
@router.message(Command('помощь'))
|
||||||
@router.message(Command('help'))
|
@router.message(Command('help'))
|
||||||
async def cmd_help(message: Message):
|
async def cmd_help(message: Message):
|
||||||
"""Справка по командам"""
|
"""Справка по командам (для жильцов)"""
|
||||||
text = '''<b>📱 Команды бота:</b>
|
from utils.ui_styles import get_colored_header, get_level_badge, get_user_level_with_ig
|
||||||
|
from database.models import InitiativeGroup
|
||||||
|
|
||||||
<b>Для всех жильцов:</b>
|
user_id = message.from_user.id
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
user = await session.get(User, user_id)
|
||||||
|
|
||||||
|
# Определяем уровень
|
||||||
|
if user and user.verified:
|
||||||
|
ig_stmt = select(InitiativeGroup).where(
|
||||||
|
InitiativeGroup.user_id == user_id,
|
||||||
|
InitiativeGroup.is_active == True
|
||||||
|
)
|
||||||
|
ig_result = await session.execute(ig_stmt)
|
||||||
|
is_ig = ig_result.scalar_one_or_none() is not None
|
||||||
|
is_admin = user.is_admin or (user_id == config.ADMIN_USER_ID)
|
||||||
|
|
||||||
|
if is_admin:
|
||||||
|
level = 'admin'
|
||||||
|
elif is_ig:
|
||||||
|
level = 'ig'
|
||||||
|
else:
|
||||||
|
level = 'verified'
|
||||||
|
else:
|
||||||
|
level = 'unverified'
|
||||||
|
|
||||||
|
# Базовые команды для ВСЕХ
|
||||||
|
text = f'''{get_colored_header('ПОМОЩЬ', level)}
|
||||||
|
|
||||||
|
{get_level_badge(level)}
|
||||||
|
|
||||||
|
<b>📱 Доступные команды:</b>
|
||||||
|
|
||||||
|
<b>Для всех:</b>
|
||||||
📞 /телефон — Экстренные службы
|
📞 /телефон — Экстренные службы
|
||||||
👷 /мастера — Сантехники, электрики
|
👷 /мастера — Сантехники, электрики
|
||||||
👮 /участковый — Контакты полиции
|
👮 /участковый — Контакты полиции
|
||||||
📅 /график — Отключения воды/света
|
📜 /правила — Правила чата
|
||||||
📢 /объявления — Новости дома
|
❓ /помощь — Эта справка
|
||||||
🔍 /моя_квартира — Проверка привязки
|
|
||||||
|
|
||||||
<b>Верификация:</b>
|
<b>Верификация:</b>
|
||||||
Просто отправьте номер квартиры текстом
|
Просто отправьте боту номер квартиры текстом
|
||||||
|
Примеры: <code>45</code>, <code>12А</code>, <code>100-1</code>
|
||||||
|
|
||||||
<b>Администратору:</b>
|
После верификации станут доступны:
|
||||||
|
• 📅 График отключений
|
||||||
|
• 💰 Платежи ЖКХ
|
||||||
|
• 🏆 Достижения
|
||||||
|
• 📋 Опросы
|
||||||
|
• 🔍 Моя квартира
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Добавляем для админа
|
||||||
|
if level == 'admin':
|
||||||
|
text += '''
|
||||||
|
<b>⚙️ Администратору:</b>
|
||||||
/admin — Панель админа
|
/admin — Панель админа
|
||||||
/stats — Статистика чата
|
/stats — Статистика чата
|
||||||
/spies — Подозрительные
|
/spies — Подозрительные
|
||||||
/export — Экспорт JSON
|
/export — Экспорт JSON
|
||||||
/users — Список жильцов'''
|
/users — Список жильцов
|
||||||
|
/verify — Верифицировать
|
||||||
|
/broadcast — Рассылка
|
||||||
|
'''
|
||||||
|
|
||||||
await message.answer(text, parse_mode='HTML', reply_markup=get_main_menu())
|
# Добавляем для ИГ
|
||||||
|
if level == 'ig':
|
||||||
|
text += '''
|
||||||
|
<b>🎯 Инициативной группе:</b>
|
||||||
|
/ig_stats — Статистика ИГ
|
||||||
|
/ig_list — Список ИГ
|
||||||
|
'''
|
||||||
|
|
||||||
|
await message.answer(text, parse_mode='HTML', reply_markup=get_main_menu(
|
||||||
|
verified=(level != 'unverified'),
|
||||||
|
is_ig=(level == 'ig'),
|
||||||
|
is_admin=(level == 'admin'),
|
||||||
|
is_superadmin=False
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == 'help')
|
@router.callback_query(F.data == 'help')
|
||||||
|
|
@ -115,19 +269,35 @@ async def cb_help(callback: CallbackQuery):
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
text = '''<b>🏠 Домовой бот — помощь</b>
|
text = '''<b>🏠 Домовой бот — помощь</b>
|
||||||
|
|
||||||
Этот бот создан для помощи жильцам нашего дома.
|
Этот бот помогает жильцам нашего дома быть в курсе событий и решать бытовые вопросы.
|
||||||
|
|
||||||
<b>📞 Экстренные телефоны</b>
|
<b>🆕 Вы впервые здесь?</b>
|
||||||
Быстрый доступ к телефонам служб
|
|
||||||
|
|
||||||
<b>👷 Мастера</b>
|
1️⃣ <b>Пройдите верификацию</b> — это просто!
|
||||||
Проверенные сантехники и электрики
|
• Отправьте боту номер вашей квартиры
|
||||||
|
• Например: <code>45</code> или <code>12А</code>
|
||||||
|
• После проверки вы получите полный доступ
|
||||||
|
|
||||||
<b>🔍 Моя квартира</b>
|
2️⃣ <b>Что будет после верификации?</b>
|
||||||
Проверить привязку к квартире
|
• 📅 График отключений воды и света
|
||||||
|
• 📢 Объявления и новости дома
|
||||||
|
• 💰 Напоминания о платежах ЖКХ
|
||||||
|
• 🏆 Рейтинг и достижения
|
||||||
|
• 📋 Участие в опросах
|
||||||
|
• 📅 События и собрания
|
||||||
|
|
||||||
<b>⏳ Верификация</b>
|
<b>📞 Телефоны служб</b>
|
||||||
Отправьте номер квартиры текстом'''
|
Нажмите "Экстренные телефоны" чтобы увидеть:
|
||||||
|
• 🚨 Аварийные службы (02, 03, 04, 112)
|
||||||
|
• 🔧 ЖКХ, сантехники, электрики
|
||||||
|
• 👮 Участковый полиция
|
||||||
|
|
||||||
|
<b>❓ Нужна помощь?</b>
|
||||||
|
Напишите администратору чата или используйте команду /help
|
||||||
|
|
||||||
|
<b>🔐 Конфиденциальность</b>
|
||||||
|
Бот видит только ваши сообщения в этом чате.
|
||||||
|
Доступ к данным есть только у администраторов.'''
|
||||||
|
|
||||||
await callback.message.answer(text, parse_mode='HTML')
|
await callback.message.answer(text, parse_mode='HTML')
|
||||||
|
|
||||||
|
|
@ -142,6 +312,90 @@ async def cb_phones(callback: CallbackQuery):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data == 'rules')
|
||||||
|
async def cb_rules(callback: CallbackQuery):
|
||||||
|
"""Правила чата"""
|
||||||
|
await callback.answer()
|
||||||
|
text = '''<b>📜 Правила нашего чата</b>
|
||||||
|
|
||||||
|
1️⃣ <b>Уважайте соседей</b>
|
||||||
|
• Не оскорбляйте и не унижайте других
|
||||||
|
• Запрещены угрозы и дискриминация
|
||||||
|
• Критикуйте идеи, а не людей
|
||||||
|
|
||||||
|
2️⃣ <b>Не спамьте</b>
|
||||||
|
• Максимум 10 сообщений в минуту
|
||||||
|
• Запрещена реклама без согласования
|
||||||
|
• Не отправляйте цепочки и фейки
|
||||||
|
|
||||||
|
3️⃣ <b>Мат — под запретом</b>
|
||||||
|
• 3 предупреждения = бан
|
||||||
|
• Используйте цензурную лексику
|
||||||
|
|
||||||
|
4️⃣ <b>Темы для обсуждений</b>
|
||||||
|
• Вопросы по дому и ЖКХ
|
||||||
|
• Соседи и взаимопомощь
|
||||||
|
• Объявления (продам, куплю, отдам)
|
||||||
|
• События и собрания
|
||||||
|
|
||||||
|
5️⃣ <b>Верификация обязательна</b>
|
||||||
|
• Отправьте номер квартиры боту
|
||||||
|
• Без верификации доступ ограничен
|
||||||
|
• Это защита от шпионов и ботов
|
||||||
|
|
||||||
|
⚠️ <b>Нарушения</b>
|
||||||
|
• Предупреждение (устно)
|
||||||
|
• Мут на 30 минут
|
||||||
|
• Бан навсегда
|
||||||
|
|
||||||
|
💡 <b>Хотите помочь чату?</b>
|
||||||
|
Вступайте в инициативную группу!'''
|
||||||
|
|
||||||
|
await callback.message.answer(text, parse_mode='HTML')
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(Command('правила'))
|
||||||
|
@router.message(Command('rules'))
|
||||||
|
async def cmd_rules(message: Message):
|
||||||
|
"""Правила чата (команда)"""
|
||||||
|
text = '''<b>📜 Правила нашего чата</b>
|
||||||
|
|
||||||
|
1️⃣ <b>Уважайте соседей</b>
|
||||||
|
• Не оскорбляйте и не унижайте других
|
||||||
|
• Запрещены угрозы и дискриминация
|
||||||
|
• Критикуйте идеи, а не людей
|
||||||
|
|
||||||
|
2️⃣ <b>Не спамьте</b>
|
||||||
|
• Максимум 10 сообщений в минуту
|
||||||
|
• Запрещена реклама без согласования
|
||||||
|
• Не отправляйте цепочки и фейки
|
||||||
|
|
||||||
|
3️⃣ <b>Мат — под запретом</b>
|
||||||
|
• 3 предупреждения = бан
|
||||||
|
• Используйте цензурную лексику
|
||||||
|
|
||||||
|
4️⃣ <b>Темы для обсуждений</b>
|
||||||
|
• Вопросы по дому и ЖКХ
|
||||||
|
• Соседи и взаимопомощь
|
||||||
|
• Объявления (продам, куплю, отдам)
|
||||||
|
• События и собрания
|
||||||
|
|
||||||
|
5️⃣ <b>Верификация обязательна</b>
|
||||||
|
• Отправьте номер квартиры боту
|
||||||
|
• Без верификации доступ ограничен
|
||||||
|
• Это защита от шпионов и ботов
|
||||||
|
|
||||||
|
⚠️ <b>Нарушения</b>
|
||||||
|
• Предупреждение (устно)
|
||||||
|
• Мут на 30 минут
|
||||||
|
• Бан навсегда
|
||||||
|
|
||||||
|
💡 <b>Хотите помочь чату?</b>
|
||||||
|
Вступайте в инициативную группу!'''
|
||||||
|
|
||||||
|
await message.answer(text, parse_mode='HTML')
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == 'phones_emergency')
|
@router.callback_query(F.data == 'phones_emergency')
|
||||||
async def cb_phones_emergency(callback: CallbackQuery):
|
async def cb_phones_emergency(callback: CallbackQuery):
|
||||||
"""Экстренные телефоны"""
|
"""Экстренные телефоны"""
|
||||||
|
|
@ -338,10 +592,50 @@ async def cb_schedule(callback: CallbackQuery):
|
||||||
@router.callback_query(F.data == 'main_menu')
|
@router.callback_query(F.data == 'main_menu')
|
||||||
async def cb_main_menu(callback: CallbackQuery):
|
async def cb_main_menu(callback: CallbackQuery):
|
||||||
"""Главное меню"""
|
"""Главное меню"""
|
||||||
|
from utils.ui_styles import get_colored_header, get_level_badge
|
||||||
|
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
|
user_id = callback.from_user.id
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
user = await session.get(User, user_id)
|
||||||
|
|
||||||
|
if user and user.verified:
|
||||||
|
# Проверяем ИГ и админа
|
||||||
|
from database.models import InitiativeGroup
|
||||||
|
ig_stmt = select(InitiativeGroup).where(
|
||||||
|
InitiativeGroup.user_id == user_id,
|
||||||
|
InitiativeGroup.is_active == True
|
||||||
|
)
|
||||||
|
ig_result = await session.execute(ig_stmt)
|
||||||
|
is_ig = ig_result.scalar_one_or_none() is not None
|
||||||
|
is_admin = user.is_admin or (user_id == config.ADMIN_USER_ID)
|
||||||
|
is_superadmin = (user_id == config.ADMIN_USER_ID)
|
||||||
|
|
||||||
|
# Определяем уровень
|
||||||
|
if is_superadmin:
|
||||||
|
level = 'superadmin'
|
||||||
|
header = get_colored_header('УПРАВЛЕНИЕ СИСТЕМОЙ', level)
|
||||||
|
elif is_admin:
|
||||||
|
level = 'admin'
|
||||||
|
header = get_colored_header('АДМИН-ПАНЕЛЬ', level)
|
||||||
|
elif is_ig:
|
||||||
|
level = 'ig'
|
||||||
|
header = get_colored_header('ИНИЦИАТИВНАЯ ГРУППА', level)
|
||||||
|
else:
|
||||||
|
level = 'verified'
|
||||||
|
header = get_colored_header('ГЛАВНОЕ МЕНЮ', level)
|
||||||
|
|
||||||
await callback.message.answer(
|
await callback.message.answer(
|
||||||
'🏠 <b>Главное меню</b>',
|
f'{header}\n\n{get_level_badge(level)}',
|
||||||
reply_markup=get_main_menu(),
|
reply_markup=get_main_menu(verified=True, is_ig=is_ig, is_admin=is_admin, is_superadmin=is_superadmin),
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
header = get_colored_header('МЕНЮ', 'unverified')
|
||||||
|
await callback.message.answer(
|
||||||
|
f'{header}\n\n{get_level_badge("unverified")}',
|
||||||
|
reply_markup=get_unverified_menu(),
|
||||||
parse_mode='HTML'
|
parse_mode='HTML'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -362,7 +656,7 @@ async def msg_masters_text(message: Message):
|
||||||
"""Текстовая команда мастера"""
|
"""Текстовая команда мастера"""
|
||||||
await message.answer(
|
await message.answer(
|
||||||
'👷 Загружаю список мастеров...',
|
'👷 Загружаю список мастеров...',
|
||||||
reply_markup=get_main_menu()
|
reply_markup=get_main_menu(verified=True)
|
||||||
)
|
)
|
||||||
# Перенаправляем на callback
|
# Перенаправляем на callback
|
||||||
async with AsyncSessionLocal() as session:
|
async with AsyncSessionLocal() as session:
|
||||||
|
|
@ -628,13 +922,205 @@ async def cb_my_apartment_menu(callback: CallbackQuery):
|
||||||
await callback.message.answer('❌ Вы не зарегистрированы')
|
await callback.message.answer('❌ Вы не зарегистрированы')
|
||||||
return
|
return
|
||||||
|
|
||||||
apt = user.apartment or '❓ Не указана'
|
apt = user.apartment or 'не указана'
|
||||||
verified = '✅' if user.verified else '⏳'
|
phone = user.phone or 'не указан'
|
||||||
|
|
||||||
text = f'🏠 <b>Ваша квартира</b>\n\n'
|
text = '🔍 <b>Ваша квартира</b>\n\n'
|
||||||
text += f'Номер: <code>{apt}</code>\n'
|
text += f'🏠 Квартира: <code>{apt}</code>\n'
|
||||||
text += f'Статус: {verified} {"Верифицирован" if user.verified else "Не верифицирован"}\n'
|
text += f'📞 Телефон: <code>{phone}</code>\n\n'
|
||||||
text += f'Рейтинг: {user.rating}\n'
|
text += '<b>Хотите изменить?</b>\n\n'
|
||||||
text += f'Уровень: {user.level}\n'
|
text += 'Используйте команды:\n'
|
||||||
|
text += '<code>/setapt 45</code> — изменить квартиру\n'
|
||||||
|
text += '<code>/setphone +79991234567</code> — изменить телефон'
|
||||||
|
|
||||||
await callback.message.answer(text, parse_mode='HTML')
|
await callback.message.answer(text, parse_mode='HTML')
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# ИЗМЕНИТЬ ПРОФИЛЬ (КОМАНДЫ)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@router.message(Command('setapt'))
|
||||||
|
@router.message(Command('set_apt'))
|
||||||
|
async def cmd_set_apt(message: Message):
|
||||||
|
"""
|
||||||
|
Изменить номер квартиры
|
||||||
|
Использование: /setapt 45
|
||||||
|
"""
|
||||||
|
from utils.validators import validate_apartment
|
||||||
|
|
||||||
|
user_id = message.from_user.id
|
||||||
|
|
||||||
|
args = message.text.split()
|
||||||
|
if len(args) < 2:
|
||||||
|
await message.answer(
|
||||||
|
'❌ <b>Использование:</b>\n'
|
||||||
|
'<code>/setapt 45</code>\n\n'
|
||||||
|
'Где 45 — ваш номер квартиры.',
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
apartment = args[1].strip().upper()
|
||||||
|
|
||||||
|
if not validate_apartment(apartment):
|
||||||
|
await message.answer(
|
||||||
|
'❌ Неверный формат квартиры.\n\n'
|
||||||
|
'Примеры: <code>45</code>, <code>12А</code>, <code>100-1</code>',
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
user = await session.get(User, user_id)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
await message.answer('❌ Вы не зарегистрированы')
|
||||||
|
return
|
||||||
|
|
||||||
|
user.apartment = apartment
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
await message.answer(
|
||||||
|
f'✅ <b>Квартира обновлена!</b>\n\n'
|
||||||
|
f'Ваша квартира: <code>{apartment}</code>',
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(Command('setphone'))
|
||||||
|
@router.message(Command('set_phone'))
|
||||||
|
async def cmd_set_phone(message: Message):
|
||||||
|
"""
|
||||||
|
Изменить номер телефона
|
||||||
|
Использование: /setphone +79991234567
|
||||||
|
"""
|
||||||
|
user_id = message.from_user.id
|
||||||
|
|
||||||
|
args = message.text.split()
|
||||||
|
if len(args) < 2:
|
||||||
|
await message.answer(
|
||||||
|
'❌ <b>Использование:</b>\n'
|
||||||
|
'<code>/setphone +79991234567</code>',
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
phone = args[1].strip()
|
||||||
|
|
||||||
|
# Простая валидация
|
||||||
|
if not phone.startswith('+') or len(phone) < 10:
|
||||||
|
await message.answer(
|
||||||
|
'❌ Неверный формат телефона.\n\n'
|
||||||
|
'Пример: <code>+79991234567</code>',
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
user = await session.get(User, user_id)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
await message.answer('❌ Вы не зарегистрированы')
|
||||||
|
return
|
||||||
|
|
||||||
|
user.phone = phone
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
await message.answer(
|
||||||
|
f'✅ <b>Телефон обновлён!</b>\n\n'
|
||||||
|
f'Ваш телефон: <code>{phone}</code>',
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# РЕДАКТИРОВАНИЕ ПРОФИЛЯ ЖИЛЬЦА (ДЛЯ АДМИНА)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@router.message(Command('edit_user'))
|
||||||
|
@router.message(Command('edituser'))
|
||||||
|
async def cmd_edit_user(message: Message):
|
||||||
|
"""
|
||||||
|
Редактировать профиль жильца (для админа)
|
||||||
|
Использование: /edit_user <user_id> [квартира] [телефон]
|
||||||
|
"""
|
||||||
|
if not config.is_admin(message.from_user.id):
|
||||||
|
await message.answer('❌ Только для администраторов')
|
||||||
|
return
|
||||||
|
|
||||||
|
args = message.text.split()
|
||||||
|
|
||||||
|
if len(args) < 2:
|
||||||
|
await message.answer(
|
||||||
|
'❌ <b>Использование:</b>\n\n'
|
||||||
|
'<code>/edit_user <user_id> [квартира] [телефон]</code>\n\n'
|
||||||
|
'Примеры:\n'
|
||||||
|
'<code>/edit_user 123456789 45 +79991234567</code>\n'
|
||||||
|
'<code>/edit_user 123456789 apt:45 phone:+79991234567</code>\n'
|
||||||
|
'<code>/edit_user 123456789 phone:+79991234567</code>\n'
|
||||||
|
'<code>/edit_user 123456789 apt:45</code>',
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_id = int(args[1])
|
||||||
|
except ValueError:
|
||||||
|
await message.answer('❌ Неверный ID пользователя')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Парсим аргументы
|
||||||
|
apartment = None
|
||||||
|
phone = None
|
||||||
|
|
||||||
|
for arg in args[2:]:
|
||||||
|
if arg.startswith('apt:'):
|
||||||
|
apartment = arg[4:].upper()
|
||||||
|
elif arg.startswith('phone:'):
|
||||||
|
phone = arg[6:]
|
||||||
|
elif apartment is None:
|
||||||
|
# Первый аргумент без префикса — квартира
|
||||||
|
apartment = arg.upper()
|
||||||
|
elif phone is None:
|
||||||
|
# Второй аргумент без префикса — телефон
|
||||||
|
phone = arg
|
||||||
|
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
user = await session.get(User, user_id)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
await message.answer(f'❌ Пользователь {user_id} не найден')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Обновляем данные
|
||||||
|
updated = []
|
||||||
|
|
||||||
|
if apartment:
|
||||||
|
user.apartment = apartment
|
||||||
|
updated.append(f'квартира: {apartment}')
|
||||||
|
|
||||||
|
if phone:
|
||||||
|
user.phone = phone
|
||||||
|
updated.append(f'телефон: {phone}')
|
||||||
|
|
||||||
|
if not updated:
|
||||||
|
await message.answer(
|
||||||
|
'❌ Укажите квартиру или телефон\n\n'
|
||||||
|
'Пример: <code>/edit_user 123456789 45 +79991234567</code>',
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
name = user.full_name or user.username or f'user{user_id}'
|
||||||
|
|
||||||
|
await message.answer(
|
||||||
|
f'✅ <b>Профиль обновлён!</b>\n\n'
|
||||||
|
f'👤 {name}\n'
|
||||||
|
f'🔖 ID: <code>{user_id}</code>\n\n'
|
||||||
|
f'Обновлено:\n' +
|
||||||
|
'\n'.join(f'• {item}' for item in updated),
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
|
|
||||||
100
import_members.py
Normal file
100
import_members.py
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Импорт всех участников группы в базу данных
|
||||||
|
Прямой запрос к Telegram API
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import aiohttp
|
||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import select
|
||||||
|
from database.db import AsyncSessionLocal, init_db
|
||||||
|
from database.models import User
|
||||||
|
from config import BOT_TOKEN, ADMIN_CHAT_ID, get_proxy_url
|
||||||
|
from aiohttp_socks import ProxyConnector
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def import_group_members():
|
||||||
|
"""Импортировать всех участников группы"""
|
||||||
|
|
||||||
|
await init_db()
|
||||||
|
|
||||||
|
proxy_url = get_proxy_url()
|
||||||
|
logger.info(f'🔒 Прокси: {proxy_url}')
|
||||||
|
|
||||||
|
# Создаём коннектор с прокси
|
||||||
|
connector = ProxyConnector.from_url(proxy_url)
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession(connector=connector) as session:
|
||||||
|
# Получаем список администраторов (это все кто есть в чате)
|
||||||
|
async with session.get(
|
||||||
|
f"https://api.telegram.org/bot{BOT_TOKEN}/getChatAdministrators",
|
||||||
|
params={"chat_id": ADMIN_CHAT_ID}
|
||||||
|
) as resp:
|
||||||
|
result = await resp.json()
|
||||||
|
|
||||||
|
if not result.get('ok'):
|
||||||
|
logger.error(f'Ошибка API: {result}')
|
||||||
|
return
|
||||||
|
|
||||||
|
members = result.get('result', [])
|
||||||
|
logger.info(f'📊 Найдено участников: {len(members)}')
|
||||||
|
|
||||||
|
async with AsyncSessionLocal() as db_session:
|
||||||
|
added = 0
|
||||||
|
updated = 0
|
||||||
|
|
||||||
|
for member in members:
|
||||||
|
# member это dict с ключом 'user'
|
||||||
|
user_data = member.get('user', {})
|
||||||
|
user_id = user_data.get('id')
|
||||||
|
|
||||||
|
if not user_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Проверяем есть ли в базе
|
||||||
|
existing = await db_session.get(User, user_id)
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
# Обновляем данные
|
||||||
|
username = user_data.get('username')
|
||||||
|
first_name = user_data.get('first_name')
|
||||||
|
last_name = user_data.get('last_name')
|
||||||
|
|
||||||
|
if username:
|
||||||
|
existing.username = username
|
||||||
|
if first_name:
|
||||||
|
existing.first_name = first_name
|
||||||
|
if last_name:
|
||||||
|
existing.last_name = last_name
|
||||||
|
# Верифицируем!
|
||||||
|
if not existing.verified:
|
||||||
|
existing.verified = True
|
||||||
|
existing.verification_date = datetime.utcnow()
|
||||||
|
updated += 1
|
||||||
|
else:
|
||||||
|
# Добавляем нового
|
||||||
|
new_user = User(
|
||||||
|
user_id=user_id,
|
||||||
|
username=user_data.get('username'),
|
||||||
|
first_name=user_data.get('first_name'),
|
||||||
|
last_name=user_data.get('last_name'),
|
||||||
|
verified=True, # Сразу верифицируем!
|
||||||
|
verification_date=datetime.utcnow(),
|
||||||
|
message_count=0,
|
||||||
|
)
|
||||||
|
db_session.add(new_user)
|
||||||
|
added += 1
|
||||||
|
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
logger.info(f'✅ Добавлено: {added}')
|
||||||
|
logger.info(f'🔄 Обновлено: {updated}')
|
||||||
|
logger.info(f'💾 Итого в базе: {added + updated}')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(import_group_members())
|
||||||
|
|
@ -3,36 +3,87 @@ Inline-клавиатуры
|
||||||
"""
|
"""
|
||||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
from utils.ui_styles import LEVEL_EMOJI
|
||||||
|
|
||||||
|
|
||||||
def get_main_menu() -> InlineKeyboardMarkup:
|
def get_main_menu(verified: bool = False, is_ig: bool = False, is_admin: bool = False, is_superadmin: bool = False) -> InlineKeyboardMarkup:
|
||||||
"""Главное меню жильца"""
|
"""
|
||||||
|
Главное меню жильца с цветными emoji
|
||||||
|
|
||||||
|
Args:
|
||||||
|
verified: Верифицирован ли пользователь
|
||||||
|
is_ig: Член инициативной группы
|
||||||
|
is_admin: Админ ли
|
||||||
|
is_superadmin: Супер-админ ли
|
||||||
|
"""
|
||||||
|
# Определяем уровень и emoji
|
||||||
|
if is_superadmin:
|
||||||
|
level_emoji = LEVEL_EMOJI['superadmin']
|
||||||
|
level = 'superadmin'
|
||||||
|
elif is_admin:
|
||||||
|
level_emoji = LEVEL_EMOJI['admin']
|
||||||
|
level = 'admin'
|
||||||
|
elif is_ig:
|
||||||
|
level_emoji = LEVEL_EMOJI['ig']
|
||||||
|
level = 'ig'
|
||||||
|
elif verified:
|
||||||
|
level_emoji = LEVEL_EMOJI['verified']
|
||||||
|
level = 'verified'
|
||||||
|
else:
|
||||||
|
level_emoji = LEVEL_EMOJI['unverified']
|
||||||
|
level = 'unverified'
|
||||||
|
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
|
|
||||||
|
# Базовые кнопки для ВСЕХ
|
||||||
builder.row(
|
builder.row(
|
||||||
InlineKeyboardButton(text='📞 Экстренные телефоны', callback_data='phones'),
|
InlineKeyboardButton(text=f'{level_emoji} 📞 Экстренные телефоны', callback_data='phones'),
|
||||||
)
|
)
|
||||||
builder.row(
|
builder.row(
|
||||||
InlineKeyboardButton(text='👷 Мастера', callback_data='masters'),
|
InlineKeyboardButton(text=f'{level_emoji} 👷 Мастера', callback_data='masters'),
|
||||||
InlineKeyboardButton(text='👮 Участковый', callback_data='police'),
|
)
|
||||||
|
|
||||||
|
# Только для верифицированных
|
||||||
|
if verified:
|
||||||
|
builder.row(
|
||||||
|
InlineKeyboardButton(text=f'{level_emoji} 📅 График отключений', callback_data='schedule'),
|
||||||
)
|
)
|
||||||
builder.row(
|
builder.row(
|
||||||
InlineKeyboardButton(text='📅 График отключений', callback_data='schedule'),
|
InlineKeyboardButton(text=f'{level_emoji} 🏆 Достижения', callback_data='achievements'),
|
||||||
|
InlineKeyboardButton(text=f'{level_emoji} 📅 События', callback_data='events'),
|
||||||
)
|
)
|
||||||
builder.row(
|
builder.row(
|
||||||
InlineKeyboardButton(text='🏆 Достижения', callback_data='achievements'),
|
InlineKeyboardButton(text=f'{level_emoji} 💰 Платежи', callback_data='payments'),
|
||||||
InlineKeyboardButton(text='📅 События', callback_data='events'),
|
InlineKeyboardButton(text=f'{level_emoji} 📋 Опросы', callback_data='polls'),
|
||||||
)
|
)
|
||||||
builder.row(
|
builder.row(
|
||||||
InlineKeyboardButton(text='💰 Платежи', callback_data='payments'),
|
InlineKeyboardButton(text=f'{level_emoji} 🔍 Моя квартира', callback_data='my_apartment'),
|
||||||
InlineKeyboardButton(text='📋 Опросы', callback_data='polls'),
|
|
||||||
)
|
)
|
||||||
builder.row(
|
builder.row(
|
||||||
InlineKeyboardButton(text='🔍 Моя квартира', callback_data='my_apartment'),
|
InlineKeyboardButton(text=f'{level_emoji} 🙏 Поблагодарить', callback_data='thank_user'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Для членов инициативной группы — дополнительные кнопки
|
||||||
|
if is_ig:
|
||||||
builder.row(
|
builder.row(
|
||||||
InlineKeyboardButton(text='🙏 Поблагодарить', callback_data='thank_user'),
|
InlineKeyboardButton(text=f'{level_emoji} 🎯 Инициативная группа', callback_data='ig_menu'),
|
||||||
InlineKeyboardButton(text='❓ Помощь', callback_data='help'),
|
)
|
||||||
|
|
||||||
|
# Для админов — кнопка админки
|
||||||
|
if is_admin:
|
||||||
|
builder.row(
|
||||||
|
InlineKeyboardButton(text=f'{level_emoji} ⚙️ Админ-панель', callback_data='admin_menu'),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Для супер-админа — особая кнопка
|
||||||
|
if is_superadmin:
|
||||||
|
builder.row(
|
||||||
|
InlineKeyboardButton(text=f'{level_emoji} 👑 УПРАВЛЕНИЕ', callback_data='superadmin_menu'),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Помощь — всегда доступна
|
||||||
|
builder.row(
|
||||||
|
InlineKeyboardButton(text=f'{level_emoji} ❓ Помощь', callback_data='help'),
|
||||||
)
|
)
|
||||||
|
|
||||||
return builder.as_markup()
|
return builder.as_markup()
|
||||||
|
|
@ -147,6 +198,28 @@ def get_phones_menu() -> InlineKeyboardMarkup:
|
||||||
return builder.as_markup()
|
return builder.as_markup()
|
||||||
|
|
||||||
|
|
||||||
|
def get_unverified_menu() -> InlineKeyboardMarkup:
|
||||||
|
"""Меню для НЕверифицированных пользователей (красный стиль)"""
|
||||||
|
builder = InlineKeyboardBuilder()
|
||||||
|
emoji = LEVEL_EMOJI['unverified'] # 🔴
|
||||||
|
|
||||||
|
builder.row(
|
||||||
|
InlineKeyboardButton(text=f'{emoji} 📞 Экстренные телефоны', callback_data='phones'),
|
||||||
|
)
|
||||||
|
builder.row(
|
||||||
|
InlineKeyboardButton(text=f'{emoji} 👷 Мастера', callback_data='masters'),
|
||||||
|
InlineKeyboardButton(text=f'{emoji} 👮 Участковый', callback_data='police'),
|
||||||
|
)
|
||||||
|
builder.row(
|
||||||
|
InlineKeyboardButton(text=f'{emoji} 📜 Правила чата', callback_data='rules'),
|
||||||
|
)
|
||||||
|
builder.row(
|
||||||
|
InlineKeyboardButton(text=f'{emoji} ❓ Помощь', callback_data='help'),
|
||||||
|
)
|
||||||
|
|
||||||
|
return builder.as_markup()
|
||||||
|
|
||||||
|
|
||||||
def get_spy_detail_menu(user_id: int) -> InlineKeyboardMarkup:
|
def get_spy_detail_menu(user_id: int) -> InlineKeyboardMarkup:
|
||||||
"""Детали шпиона"""
|
"""Детали шпиона"""
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
|
|
|
||||||
137
utils/ui_styles.py
Normal file
137
utils/ui_styles.py
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
"""
|
||||||
|
UI Styles — стили оформления для разных уровней пользователей
|
||||||
|
"""
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# ЦВЕТА И EMOJI ДЛЯ УРОВНЕЙ
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
LEVEL_COLORS = {
|
||||||
|
'unverified': 'red', # Неверифицированные
|
||||||
|
'verified': 'green', # Верифицированные
|
||||||
|
'ig': 'blue', # Инициативная группа
|
||||||
|
'admin': 'orange', # Админ
|
||||||
|
'superadmin': 'purple', # Супер-админ (владелец)
|
||||||
|
}
|
||||||
|
|
||||||
|
LEVEL_EMOJI = {
|
||||||
|
'unverified': '🔴',
|
||||||
|
'verified': '🟢',
|
||||||
|
'ig': '🔵',
|
||||||
|
'admin': '🟡',
|
||||||
|
'superadmin': '🟣',
|
||||||
|
}
|
||||||
|
|
||||||
|
LEVEL_NAMES = {
|
||||||
|
'unverified': 'НОВИЧОК',
|
||||||
|
'verified': 'ЖИЛЕЦ',
|
||||||
|
'ig': 'АКТИВ',
|
||||||
|
'admin': 'АДМИН',
|
||||||
|
'superadmin': 'ВЛАДЕЛЕЦ',
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# ФОРМАТИРОВАНИЕ ТЕКСТА
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def get_colored_header(text: str, level: str) -> str:
|
||||||
|
"""
|
||||||
|
Создать заголовок с emoji (без <font> тегов!)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: текст заголовка
|
||||||
|
level: уровень пользователя
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
строка с emoji и жирным текстом
|
||||||
|
"""
|
||||||
|
emoji = LEVEL_EMOJI.get(level, '⚪')
|
||||||
|
|
||||||
|
return f'{emoji} <b>{text}</b>'
|
||||||
|
|
||||||
|
|
||||||
|
def get_level_badge(level: str) -> str:
|
||||||
|
"""
|
||||||
|
Получить бейдж уровня (без HTML тегов!)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
level: уровень пользователя
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
строка с emoji и названием
|
||||||
|
"""
|
||||||
|
emoji = LEVEL_EMOJI.get(level, '⚪')
|
||||||
|
name = LEVEL_NAMES.get(level, 'НЕИЗВЕСТНО')
|
||||||
|
|
||||||
|
return f'<b>{emoji} {name}</b>'
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_level(user) -> str:
|
||||||
|
"""
|
||||||
|
Определить уровень пользователя
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user: объект User
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
строка уровня (unverified, verified, ig, admin, superadmin)
|
||||||
|
"""
|
||||||
|
from config import ADMIN_USER_ID
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return 'unverified'
|
||||||
|
|
||||||
|
# Супер-админ (владелец)
|
||||||
|
if user.user_id == ADMIN_USER_ID:
|
||||||
|
return 'superadmin'
|
||||||
|
|
||||||
|
# Админ
|
||||||
|
if user.is_admin:
|
||||||
|
return 'admin'
|
||||||
|
|
||||||
|
# Инициативная группа
|
||||||
|
# Проверяется через InitiativeGroup.is_member()
|
||||||
|
# Но для простоты вернём verified если не ИГ
|
||||||
|
|
||||||
|
# Верифицированный
|
||||||
|
if user.verified:
|
||||||
|
return 'verified'
|
||||||
|
|
||||||
|
# Неверифицированный
|
||||||
|
return 'unverified'
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_level_with_ig(user, is_ig_member: bool = False) -> str:
|
||||||
|
"""
|
||||||
|
Определить уровень пользователя с учётом ИГ
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user: объект User
|
||||||
|
is_ig_member: член ИГ
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
строка уровня
|
||||||
|
"""
|
||||||
|
from config import ADMIN_USER_ID
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return 'unverified'
|
||||||
|
|
||||||
|
# Супер-админ (владелец)
|
||||||
|
if user.user_id == ADMIN_USER_ID:
|
||||||
|
return 'superadmin'
|
||||||
|
|
||||||
|
# Админ
|
||||||
|
if user.is_admin:
|
||||||
|
return 'admin'
|
||||||
|
|
||||||
|
# Инициативная группа
|
||||||
|
if is_ig_member and user.verified:
|
||||||
|
return 'ig'
|
||||||
|
|
||||||
|
# Верифицированный
|
||||||
|
if user.verified:
|
||||||
|
return 'verified'
|
||||||
|
|
||||||
|
# Неверифицированный
|
||||||
|
return 'unverified'
|
||||||
200
web/app.py
200
web/app.py
|
|
@ -75,26 +75,50 @@ async def dashboard(request: Request, username: str = Depends(get_current_admin)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
@app.get("/users", response_class=HTMLResponse)
|
@app.get("/users", response_class=HTMLResponse)
|
||||||
async def users_page(request: Request, search: str = "", username: str = Depends(get_current_admin)):
|
async def users_page(request: Request, search: str = "", filter: str = "", username: str = Depends(get_current_admin)):
|
||||||
"""Пользователи"""
|
"""Пользователи с фильтрами"""
|
||||||
async with AsyncSessionLocal() as session:
|
async with AsyncSessionLocal() as session:
|
||||||
stmt = select(User)
|
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)
|
||||||
|
# filter == 'all' или пустой — показываем всех
|
||||||
|
|
||||||
|
# Поиск
|
||||||
if search:
|
if search:
|
||||||
stmt = stmt.where((User.full_name.ilike(f"%{search}%")) |
|
stmt = stmt.where(
|
||||||
(User.apartment.ilike(f"%{search}%")))
|
(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)
|
stmt = stmt.order_by(User.user_id.desc()).limit(100)
|
||||||
users = list((await session.execute(stmt)).scalars().all())
|
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())
|
||||||
|
|
||||||
# Статистика
|
# Статистика
|
||||||
total_count = (await session.execute(select(func.count(User.user_id)))).scalar()
|
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()
|
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(
|
active_unverified = (await session.execute(
|
||||||
select(func.count(User.user_id)).where(User.verified == False, User.message_count >= 50)
|
select(func.count(User.user_id)).where(User.verified == False, User.message_count >= 50)
|
||||||
)).scalar()
|
)).scalar()
|
||||||
|
|
||||||
return templates.TemplateResponse("users.html",
|
return templates.TemplateResponse("users.html",
|
||||||
{"request": request, "username": username, "users": users, "search": search,
|
{"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}})
|
"stats": {"total": total_count, "verified": verified_count, "unverified": unverified_count, "active_unverified": active_unverified},
|
||||||
|
"ig_user_ids": ig_user_ids})
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/users/auto_verify")
|
@app.post("/api/users/auto_verify")
|
||||||
|
|
@ -139,6 +163,31 @@ async def api_update_user(user_id: int, request: Request,
|
||||||
return JSONResponse({"success": True})
|
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")
|
@app.post("/api/user/{user_id}/ban")
|
||||||
async def api_ban_user(user_id: int, username: str = Depends(get_current_admin)):
|
async def api_ban_user(user_id: int, username: str = Depends(get_current_admin)):
|
||||||
"""Забанить"""
|
"""Забанить"""
|
||||||
|
|
@ -176,6 +225,54 @@ async def api_unverify_user(user_id: int, username: str = Depends(get_current_ad
|
||||||
return JSONResponse({"success": True, "message": "Верификация отозвана"})
|
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)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# ВЕРИФИКАЦИЯ
|
# ВЕРИФИКАЦИЯ
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
@ -392,26 +489,25 @@ async def broadcast_page(request: Request, username: str = Depends(get_current_a
|
||||||
async def api_send_broadcast(
|
async def api_send_broadcast(
|
||||||
text: str = Form(...),
|
text: str = Form(...),
|
||||||
photo: UploadFile = File(None),
|
photo: UploadFile = File(None),
|
||||||
|
recipients: str = Form("all_and_chat"),
|
||||||
username: str = Depends(get_current_admin)
|
username: str = Depends(get_current_admin)
|
||||||
):
|
):
|
||||||
"""Отправить рассылку"""
|
"""Отправить рассылку"""
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from services.initiative_group import InitiativeGroupService
|
||||||
|
|
||||||
photo_id = None
|
photo_id = None
|
||||||
|
|
||||||
# Если есть картинка — загружаем
|
# Если есть картинка — загружаем
|
||||||
if photo and photo.filename:
|
if photo and photo.filename:
|
||||||
# Сохраняем временно
|
|
||||||
temp_path = Path("data") / f"broadcast_{photo.filename}"
|
temp_path = Path("data") / f"broadcast_{photo.filename}"
|
||||||
with open(temp_path, "wb") as f:
|
with open(temp_path, "wb") as f:
|
||||||
content = await photo.read()
|
content = await photo.read()
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
# Загружаем в Telegram
|
|
||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
# Отправляем фото чтобы получить file_id
|
|
||||||
url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/sendPhoto"
|
url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/sendPhoto"
|
||||||
data = aiohttp.FormData()
|
data = aiohttp.FormData()
|
||||||
data.add_field('chat_id', config.ADMIN_USER_ID)
|
data.add_field('chat_id', config.ADMIN_USER_ID)
|
||||||
|
|
@ -425,16 +521,29 @@ async def api_send_broadcast(
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка загрузки фото: {e}")
|
logger.error(f"Ошибка загрузки фото: {e}")
|
||||||
|
|
||||||
# Рассылаем
|
# Получаем получателей
|
||||||
async with AsyncSessionLocal() as session:
|
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))
|
||||||
|
users = list((await session.execute(stmt)).scalars().all())
|
||||||
|
elif recipients == 'all_verified':
|
||||||
|
# Все верифицированные
|
||||||
stmt = select(User).where(User.verified == True)
|
stmt = select(User).where(User.verified == True)
|
||||||
result = await session.execute(stmt)
|
users = list((await session.execute(stmt)).scalars().all())
|
||||||
users = list(result.scalars().all())
|
else:
|
||||||
|
# Все верифицированные (по умолчанию)
|
||||||
|
stmt = select(User).where(User.verified == True)
|
||||||
|
users = list((await session.execute(stmt)).scalars().all())
|
||||||
|
|
||||||
success_count = 0
|
success_count = 0
|
||||||
error_count = 0
|
error_count = 0
|
||||||
|
chat_sent = False
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as http_session:
|
async with aiohttp.ClientSession() as http_session:
|
||||||
|
# Отправка пользователям
|
||||||
for user in users:
|
for user in users:
|
||||||
try:
|
try:
|
||||||
url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/"
|
url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/"
|
||||||
|
|
@ -459,9 +568,40 @@ async def api_send_broadcast(
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_count += 1
|
error_count += 1
|
||||||
|
|
||||||
|
# Отправка в чат (если нужно)
|
||||||
|
if recipients in ['all_and_chat', 'chat_only']:
|
||||||
|
try:
|
||||||
|
url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/"
|
||||||
|
if photo_id:
|
||||||
|
url += "sendPhoto"
|
||||||
|
data = aiohttp.FormData()
|
||||||
|
data.add_field('chat_id', config.ADMIN_CHAT_ID)
|
||||||
|
data.add_field('photo', photo_id)
|
||||||
|
data.add_field('caption', f"📢 Объявление от администрации\n\n{text}")
|
||||||
|
async with http_session.post(url, data=data, timeout=30) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
chat_sent = True
|
||||||
|
else:
|
||||||
|
url += "sendMessage"
|
||||||
|
data = aiohttp.FormData()
|
||||||
|
data.add_field('chat_id', config.ADMIN_CHAT_ID)
|
||||||
|
data.add_field('text', f"📢 Объявление от администрации\n\n{text}")
|
||||||
|
data.add_field('parse_mode', 'HTML')
|
||||||
|
async with http_session.post(url, data=data, timeout=30) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
chat_sent = True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка отправки в чат: {e}")
|
||||||
|
|
||||||
return JSONResponse({
|
return JSONResponse({
|
||||||
"success": True,
|
"success": True,
|
||||||
"message": f"Отправлено: {success_count}, Ошибок: {error_count}"
|
"message": f"Отправлено: {success_count} (в чат: {'✅' if chat_sent else '❌'})",
|
||||||
|
"details": {
|
||||||
|
"users_sent": success_count,
|
||||||
|
"errors": error_count,
|
||||||
|
"chat_sent": chat_sent,
|
||||||
|
"recipients": recipients
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -607,6 +747,40 @@ async def api_export_json(username: str = Depends(get_current_admin)):
|
||||||
# НАСТРОЙКИ
|
# НАСТРОЙКИ
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
@app.get("/settings", response_class=HTMLResponse)
|
||||||
|
async def settings_page(request: Request, username: str = Depends(get_current_admin)):
|
||||||
|
"""Страница настроек"""
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
from sqlalchemy import text
|
||||||
|
result = await session.execute(text("SELECT key, value FROM settings"))
|
||||||
|
settings = {row[0]: row[1] for row in result.all()}
|
||||||
|
|
||||||
|
return templates.TemplateResponse("settings.html", {
|
||||||
|
"request": request,
|
||||||
|
"username": username,
|
||||||
|
"settings": settings
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@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("/settings", response_class=HTMLResponse)
|
@app.get("/settings", response_class=HTMLResponse)
|
||||||
async def settings_page(request: Request, username: str = Depends(get_current_admin)):
|
async def settings_page(request: Request, username: str = Depends(get_current_admin)):
|
||||||
"""Настройки"""
|
"""Настройки"""
|
||||||
|
|
|
||||||
|
|
@ -60,9 +60,20 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Кому отправить:</label>
|
||||||
|
<select class="form-select" id="broadcastRecipients">
|
||||||
|
<option value="all_verified">✅ Всем верифицированным</option>
|
||||||
|
<option value="ig_only">🔵 Только Инициативной Группе</option>
|
||||||
|
<option value="chat_only">📢 Только в общий чат</option>
|
||||||
|
<option value="all_and_chat">✅ Всем + в чат (по умолчанию)</option>
|
||||||
|
</select>
|
||||||
|
<small class="text-muted">Выберите получателей рассылки</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<i class="bi bi-info-circle"></i>
|
<i class="bi bi-info-circle"></i>
|
||||||
<b>Получатели:</b> Все верифицированные пользователи + общий чат
|
<b>Получатели:</b> <span id="recipientsInfo">Все верифицированные пользователи + общий чат</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary btn-lg">
|
<button type="submit" class="btn btn-primary btn-lg">
|
||||||
|
|
@ -91,6 +102,17 @@
|
||||||
</div>
|
</div>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
// Обновление информации о получателях
|
||||||
|
document.getElementById('broadcastRecipients').addEventListener('change', function() {
|
||||||
|
const info = {
|
||||||
|
'all_verified': 'Все верифицированные пользователи (в личку)',
|
||||||
|
'ig_only': 'Только участники Инициативной Группы (в личку)',
|
||||||
|
'chat_only': 'Только в общий чат (одно сообщение)',
|
||||||
|
'all_and_chat': 'Все верифицированные (в личку) + общий чат'
|
||||||
|
};
|
||||||
|
document.getElementById('recipientsInfo').textContent = info[this.value] || '';
|
||||||
|
});
|
||||||
|
|
||||||
function previewImage() {
|
function previewImage() {
|
||||||
const file = document.getElementById('broadcastPhoto').files[0];
|
const file = document.getElementById('broadcastPhoto').files[0];
|
||||||
const preview = document.getElementById('preview');
|
const preview = document.getElementById('preview');
|
||||||
|
|
@ -113,6 +135,7 @@
|
||||||
|
|
||||||
const text = document.getElementById('broadcastText').value;
|
const text = document.getElementById('broadcastText').value;
|
||||||
const photo = document.getElementById('broadcastPhoto').files[0];
|
const photo = document.getElementById('broadcastPhoto').files[0];
|
||||||
|
const recipients = document.getElementById('broadcastRecipients').value;
|
||||||
const resultDiv = document.getElementById('result');
|
const resultDiv = document.getElementById('result');
|
||||||
|
|
||||||
if (!text.trim()) {
|
if (!text.trim()) {
|
||||||
|
|
@ -120,12 +143,20 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!confirm('Отправить рассылку всем верифицированным пользователям?')) return;
|
const recipientNames = {
|
||||||
|
'all_verified': 'всем верифицированным',
|
||||||
|
'ig_only': 'Инициативной Группе',
|
||||||
|
'chat_only': 'в общий чат',
|
||||||
|
'all_and_chat': 'всем верифицированным + в чат'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!confirm(`Отправить рассылку ${recipientNames[recipients] || 'всем'}?`)) return;
|
||||||
|
|
||||||
resultDiv.innerHTML = '<div class="alert alert-info">📤 Отправка рассылки...</div>';
|
resultDiv.innerHTML = '<div class="alert alert-info">📤 Отправка рассылки...</div>';
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('text', text);
|
formData.append('text', text);
|
||||||
|
formData.append('recipients', recipients);
|
||||||
if (photo) {
|
if (photo) {
|
||||||
formData.append('photo', photo);
|
formData.append('photo', photo);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -122,28 +122,36 @@
|
||||||
<!-- Stats Cards -->
|
<!-- Stats Cards -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
|
<a href="/users?filter=all" style="text-decoration: none;">
|
||||||
<div class="stat-card primary">
|
<div class="stat-card primary">
|
||||||
<div class="stat-value">{{ stats.users }}</div>
|
<div class="stat-value">{{ stats.users }}</div>
|
||||||
<div class="stat-label">Пользователей</div>
|
<div class="stat-label">Пользователей</div>
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
|
<a href="/users?filter=verified" style="text-decoration: none;">
|
||||||
<div class="stat-card success">
|
<div class="stat-card success">
|
||||||
<div class="stat-value">{{ stats.verified }}</div>
|
<div class="stat-value">{{ stats.verified }}</div>
|
||||||
<div class="stat-label">Верифицировано</div>
|
<div class="stat-label">Верифицировано</div>
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
|
<a href="/users?filter=unverified" style="text-decoration: none;">
|
||||||
<div class="stat-card warning">
|
<div class="stat-card warning">
|
||||||
<div class="stat-value">{{ stats.messages }}</div>
|
<div class="stat-value">{{ stats.unverified }}</div>
|
||||||
<div class="stat-label">Сообщений</div>
|
<div class="stat-label">Не верифицировано</div>
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
|
<a href="/users?filter=active_unverified" style="text-decoration: none;">
|
||||||
<div class="stat-card danger">
|
<div class="stat-card danger">
|
||||||
<div class="stat-value">{{ stats.polls }}</div>
|
<div class="stat-value">{{ stats.active_unverified }}</div>
|
||||||
<div class="stat-label">Активных опросов</div>
|
<div class="stat-label">Активных (50+ сообщ.)</div>
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,11 @@
|
||||||
<html lang="ru">
|
<html lang="ru">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Настройки - Домовой Бот</title>
|
<title>Настройки - Домовой Бот</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
.sidebar {
|
.sidebar { min-height: 100vh; background: linear-gradient(180deg, #2c3e50 0%, #1a252f 100%); }
|
||||||
min-height: 100vh;
|
|
||||||
background: linear-gradient(180deg, #2c3e50 0%, #1a252f 100%);
|
|
||||||
}
|
|
||||||
.sidebar .nav-link { color: rgba(255,255,255,0.8); padding: 1rem; }
|
.sidebar .nav-link { color: rgba(255,255,255,0.8); padding: 1rem; }
|
||||||
.sidebar .nav-link:hover, .sidebar .nav-link.active { color: white; background-color: rgba(255,255,255,0.1); }
|
.sidebar .nav-link:hover, .sidebar .nav-link.active { color: white; background-color: rgba(255,255,255,0.1); }
|
||||||
.content { padding: 2rem; }
|
.content { padding: 2rem; }
|
||||||
|
|
@ -26,54 +22,82 @@
|
||||||
<nav class="nav flex-column">
|
<nav class="nav flex-column">
|
||||||
<a class="nav-link" href="/"><i class="bi bi-speedometer2"></i> Дашборд</a>
|
<a class="nav-link" href="/"><i class="bi bi-speedometer2"></i> Дашборд</a>
|
||||||
<a class="nav-link" href="/users"><i class="bi bi-people"></i> Пользователи</a>
|
<a class="nav-link" href="/users"><i class="bi bi-people"></i> Пользователи</a>
|
||||||
<a class="nav-link" href="/export"><i class="bi bi-download"></i> Экспорт</a>
|
<a class="nav-link" href="/verification"><i class="bi bi-shield-check"></i> Верификация</a>
|
||||||
|
<a class="nav-link" href="/phones"><i class="bi bi-telephone"></i> Телефоны</a>
|
||||||
|
<a class="nav-link" href="/broadcast"><i class="bi bi-broadcast"></i> Рассылки</a>
|
||||||
|
<a class="nav-link" href="/polls"><i class="bi bi-bar-chart"></i> Опросы</a>
|
||||||
|
<a class="nav-link" href="/events"><i class="bi bi-calendar-event"></i> События</a>
|
||||||
|
<a class="nav-link" href="/schedules"><i class="bi bi-clock"></i> Расписания</a>
|
||||||
<a class="nav-link active" href="/settings"><i class="bi bi-gear"></i> Настройки</a>
|
<a class="nav-link active" href="/settings"><i class="bi bi-gear"></i> Настройки</a>
|
||||||
|
<a class="nav-link" href="/export"><i class="bi bi-download"></i> Экспорт</a>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-10 content">
|
<div class="col-md-10 content">
|
||||||
<h2 class="mb-4">Настройки</h2>
|
<h2 class="mb-4"><i class="bi bi-gear"></i> Настройки бота</h2>
|
||||||
|
|
||||||
<div class="card">
|
<!-- IG Broadcast Limit -->
|
||||||
<div class="card-header">
|
<div class="card mb-4">
|
||||||
<i class="bi bi-info-circle"></i> Текущая конфигурация
|
<div class="card-header bg-info text-white">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-people"></i> Рассылка Инициативной Группы</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<table class="table">
|
<form id="igBroadcastForm">
|
||||||
<tr>
|
<div class="mb-3">
|
||||||
<th>Bot Token:</th>
|
<label class="form-label">Минимальный интервал между рассылками (секунды)</label>
|
||||||
<td><code>{{ config.bot_token }}</code></td>
|
<input type="number" class="form-control" id="igBroadcastLimit" value="{{ settings.ig_broadcast_limit }}" min="0" step="60">
|
||||||
</tr>
|
<small class="text-muted">
|
||||||
<tr>
|
<ul class="mb-0">
|
||||||
<th>Admin Chat ID:</th>
|
<li><code>0</code> — Без ограничений (можно спамить!)</li>
|
||||||
<td><code>{{ config.admin_chat_id }}</code></td>
|
<li><code>3600</code> — 1 раз в час</li>
|
||||||
</tr>
|
<li><code>86400</code> — 1 раз в сутки</li>
|
||||||
<tr>
|
</ul>
|
||||||
<th>Admin User ID:</th>
|
</small>
|
||||||
<td><code>{{ config.admin_user_id }}</code></td>
|
</div>
|
||||||
</tr>
|
<button type="submit" class="btn btn-primary">
|
||||||
<tr>
|
<i class="bi bi-save"></i> Сохранить
|
||||||
<th>Прокси:</th>
|
</button>
|
||||||
<td>
|
</form>
|
||||||
{% if config.use_proxy %}
|
<div id="igBroadcastResult" class="mt-3"></div>
|
||||||
<span class="badge bg-success">✅ Включен</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-danger">❌ Отключен</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-info mt-4">
|
<!-- Info -->
|
||||||
<i class="bi bi-info-circle"></i>
|
<div class="card">
|
||||||
Для изменения настроек отредактируйте файл <code>.env</code> и перезапустите бота.
|
<div class="card-body">
|
||||||
|
<h5>ℹ️ Информация</h5>
|
||||||
|
<p>Здесь будут настройки бота для суперадмина.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script>
|
||||||
|
document.getElementById('igBroadcastForm').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const limit = parseInt(document.getElementById('igBroadcastLimit').value);
|
||||||
|
const resultDiv = document.getElementById('igBroadcastResult');
|
||||||
|
|
||||||
|
resultDiv.innerHTML = '<div class="alert alert-info">💾 Сохранение...</div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/settings/ig_broadcast_limit', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ limit: limit })
|
||||||
|
});
|
||||||
|
const result = await r.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
resultDiv.innerHTML = '<div class="alert alert-success">✅ Настройка сохранена!</div>';
|
||||||
|
} else {
|
||||||
|
resultDiv.innerHTML = '<div class="alert alert-danger">❌ Ошибка: ' + result.error + '</div>';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
resultDiv.innerHTML = '<div class="alert alert-danger">❌ Ошибка: ' + err.message + '</div>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -34,42 +34,68 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-10 content">
|
<div class="col-md-10 content">
|
||||||
<h2 class="mb-4">Пользователи</h2>
|
<h2 class="mb-4">
|
||||||
|
Пользователи
|
||||||
|
{% if current_filter %}
|
||||||
|
<span class="badge bg-info ms-2">
|
||||||
|
{% if current_filter == 'verified' %}
|
||||||
|
✅ Верифицированные
|
||||||
|
{% elif current_filter == 'unverified' %}
|
||||||
|
⏳ Не верифицированы
|
||||||
|
{% elif current_filter == 'active_unverified' %}
|
||||||
|
🔥 Активные (50+ сообщ.)
|
||||||
|
{% else %}
|
||||||
|
Все
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
<a href="/users" class="btn btn-sm btn-outline-secondary ms-2">
|
||||||
|
<i class="bi bi-x"></i> Сбросить фильтр
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</h2>
|
||||||
|
|
||||||
<!-- Stats -->
|
<!-- Stats -->
|
||||||
{% if stats %}
|
{% if stats %}
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
|
<a href="/users" style="text-decoration: none;">
|
||||||
<div class="card bg-primary text-white">
|
<div class="card bg-primary text-white">
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
<h3>{{ stats.total }}</h3>
|
<h3>{{ stats.total }}</h3>
|
||||||
<small>Всего пользователей</small>
|
<small>Всего пользователей</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
|
<a href="/users?filter=verified" style="text-decoration: none;">
|
||||||
<div class="card bg-success text-white">
|
<div class="card bg-success text-white">
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
<h3>{{ stats.verified }}</h3>
|
<h3>{{ stats.verified }}</h3>
|
||||||
<small>Верифицировано</small>
|
<small>Верифицировано</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
|
<a href="/users?filter=unverified" style="text-decoration: none;">
|
||||||
<div class="card bg-warning text-dark">
|
<div class="card bg-warning text-dark">
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
<h3>{{ stats.unverified }}</h3>
|
<h3>{{ stats.unverified }}</h3>
|
||||||
<small>Не верифицировано</small>
|
<small>Не верифицировано</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
|
<a href="/users?filter=active_unverified" style="text-decoration: none;">
|
||||||
<div class="card bg-info text-white">
|
<div class="card bg-info text-white">
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
<h3>{{ stats.active_unverified }}</h3>
|
<h3>{{ stats.active_unverified }}</h3>
|
||||||
<small>Активных (50+ сообщений)</small>
|
<small>Активных (50+ сообщений)</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -113,6 +139,43 @@
|
||||||
|
|
||||||
<!-- Users Table -->
|
<!-- Users Table -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<h5 class="mb-0">📋 Пользователи</h5>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 text-end">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#legendCollapse">
|
||||||
|
ℹ️ Легенда
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="collapse" id="legendCollapse">
|
||||||
|
<div class="card-body bg-light">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6 class="mb-2">🔘 Кнопки действий:</h6>
|
||||||
|
<ul class="list-unstyled mb-0">
|
||||||
|
<li><span class="btn btn-sm btn-primary"><i class="bi bi-pencil"></i></span> — Редактировать профиль</li>
|
||||||
|
<li><span class="btn btn-sm btn-success"><i class="bi bi-shield-check"></i></span> — Верифицировать</li>
|
||||||
|
<li><span class="btn btn-sm btn-warning"><i class="bi bi-shield-x"></i></span> — Отозвать верификацию</li>
|
||||||
|
<li><span class="btn btn-sm btn-danger"><i class="bi bi-lock"></i></span> — Забанить</li>
|
||||||
|
<li><span class="btn btn-sm btn-success"><i class="bi bi-unlock"></i></span> — Разбанить</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6 class="mb-2">👥 Управление доступом:</h6>
|
||||||
|
<ul class="list-unstyled mb-0">
|
||||||
|
<li><span class="btn btn-sm btn-dark"><i class="bi bi-person-check"></i></span> — Назначить админом</li>
|
||||||
|
<li><span class="btn btn-sm btn-outline-dark"><i class="bi bi-person-x"></i></span> — Снять админа</li>
|
||||||
|
<li><span class="btn btn-sm btn-info"><i class="bi bi-person-plus"></i></span> — Добавить в ИГ</li>
|
||||||
|
<li><span class="btn btn-sm btn-outline-info"><i class="bi bi-person-dash"></i></span> — Удалить из ИГ</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
|
|
@ -130,6 +193,7 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
|
{% set is_in_ig = user.user_id in ig_user_ids %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ user.user_id }}</td>
|
<td>{{ user.user_id }}</td>
|
||||||
<td>{{ user.full_name or '—' }}</td>
|
<td>{{ user.full_name or '—' }}</td>
|
||||||
|
|
@ -177,6 +241,24 @@
|
||||||
<i class="bi bi-shield-check"></i>
|
<i class="bi bi-shield-check"></i>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if user.is_admin %}
|
||||||
|
<button class="btn btn-sm btn-outline-dark" onclick="removeAdmin({{ user.user_id }})" title="Снять админа">
|
||||||
|
<i class="bi bi-person-x"></i>
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<button class="btn btn-sm btn-dark" onclick="makeAdmin({{ user.user_id }})" title="Назначить админом">
|
||||||
|
<i class="bi bi-person-check"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% if is_in_ig %}
|
||||||
|
<button class="btn btn-sm btn-outline-info" onclick="removeFromIg({{ user.user_id }})" title="Удалить из ИГ">
|
||||||
|
<i class="bi bi-person-dash"></i>
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<button class="btn btn-sm btn-info" onclick="addToIg({{ user.user_id }})" title="Добавить в ИГ">
|
||||||
|
<i class="bi bi-person-plus"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
@ -306,6 +388,50 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function makeAdmin(userId) {
|
||||||
|
if (!confirm('Назначить пользователем админом?')) return;
|
||||||
|
const r = await fetch(`/api/user/${userId}/make_admin`, { method: 'POST' });
|
||||||
|
const result = await r.json();
|
||||||
|
if (result.success) {
|
||||||
|
alert('✅ Пользователь теперь админ!');
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('❌ Ошибка: ' + result.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeAdmin(userId) {
|
||||||
|
if (!confirm('Снять админа?')) return;
|
||||||
|
const r = await fetch(`/api/user/${userId}/remove_admin`, { method: 'POST' });
|
||||||
|
const result = await r.json();
|
||||||
|
if (result.success) {
|
||||||
|
alert('✅ Админ снят');
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addToIg(userId) {
|
||||||
|
if (!confirm('Добавить в Инициативную Группу?')) return;
|
||||||
|
const r = await fetch(`/api/user/${userId}/add_to_ig`, { method: 'POST' });
|
||||||
|
const result = await r.json();
|
||||||
|
if (result.success) {
|
||||||
|
alert('✅ Добавлен в ИГ!');
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('❌ Ошибка: ' + result.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeFromIg(userId) {
|
||||||
|
if (!confirm('Удалить из Инициативной Группы?')) return;
|
||||||
|
const r = await fetch(`/api/user/${userId}/remove_from_ig`, { method: 'POST' });
|
||||||
|
const result = await r.json();
|
||||||
|
if (result.success) {
|
||||||
|
alert('✅ Удалён из ИГ');
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function autoVerify() {
|
async function autoVerify() {
|
||||||
if (!confirm('Верифицировать всех активных пользователей (50+ сообщений)?')) return;
|
if (!confirm('Верифицировать всех активных пользователей (50+ сообщений)?')) return;
|
||||||
|
|
||||||
|
|
|
||||||
103
ИНСТРУКЦИЯ_ДЛЯ_ЖИЛЬЦОВ.md
Normal file
103
ИНСТРУКЦИЯ_ДЛЯ_ЖИЛЬЦОВ.md
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
# 🏠 ДОМОВОЙ БОТ — ИНСТРУКЦИЯ ДЛЯ ЖИЛЬЦОВ
|
||||||
|
|
||||||
|
**Добро пожаловать в чат нашего дома!**
|
||||||
|
|
||||||
|
Бот помогает жильцам общаться, решать вопросы и быть в курсе событий.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 ПЕРВЫЕ ШАГИ
|
||||||
|
|
||||||
|
### 1️⃣ Запустите бота
|
||||||
|
Нажмите `/start` в личном сообщении боту [@lkmthreeseven_bot](https://t.me/lkmthreeseven_bot)
|
||||||
|
|
||||||
|
### 2️⃣ Пройдите верификацию
|
||||||
|
1. Нажмите **"📋 Верификация"**
|
||||||
|
2. Укажите номер квартиры (например: `123`)
|
||||||
|
3. Укажите телефон (например: `+79991234567`)
|
||||||
|
4. Дождитесь подтверждения от админа
|
||||||
|
|
||||||
|
**Без верификации доступ к функциям ограничен!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 ОСНОВНЫЕ КОМАНДЫ
|
||||||
|
|
||||||
|
### 🔰 Главное меню
|
||||||
|
```
|
||||||
|
/start — Главное меню
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📢 Объявления
|
||||||
|
```
|
||||||
|
/ad — Создать объявление
|
||||||
|
/ads — Все объявления (продам, куплю, отдам, услуга)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📅 События и график
|
||||||
|
```
|
||||||
|
/events — Календарь событий дома
|
||||||
|
/график — Расписание отключений (вода, свет, отопление)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 💰 Платежи
|
||||||
|
```
|
||||||
|
/payments — Напоминания о платежах ЖКХ
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📊 Рейтинг и достижения
|
||||||
|
```
|
||||||
|
/rating — Мой рейтинг активности
|
||||||
|
/achievements — Мои достижения
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📞 Телефоны
|
||||||
|
```
|
||||||
|
/phones — Телефоны экстренных служб и мастеров
|
||||||
|
```
|
||||||
|
|
||||||
|
### ℹ️ Помощь
|
||||||
|
```
|
||||||
|
/help — Справка по командам
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛡️ ПРАВИЛА ЧАТА
|
||||||
|
|
||||||
|
| Нарушение | Последствие |
|
||||||
|
|-----------|-------------|
|
||||||
|
| Мат/оскорбления | ⚠️ Предупреждение (3 → бан) |
|
||||||
|
| Спам ссылками | ⚠️ Удаление + предупреждение |
|
||||||
|
| Флуд (10+ сообщений за 1 мин) | ⚠️ Мут на 30 мин |
|
||||||
|
| Угрозы, дискриминация | 🚫 Бан без предупреждения |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 ЧТО УМЕЕТ БОТ
|
||||||
|
|
||||||
|
✅ **Доска объявлений** — продай, купи, отдай вещи
|
||||||
|
✅ **Опросы и голосования** — решаем вопросы вместе
|
||||||
|
✅ **Напоминания о платежах** — 5 и 9 числа в 09:00
|
||||||
|
✅ **Расписание отключений** — будь в курсе
|
||||||
|
✅ **Рейтинг активности** — стань лидером дома!
|
||||||
|
✅ **Достижения** — получай за активность
|
||||||
|
✅ **Детект шпионов** — защита от ботов
|
||||||
|
✅ **Анти-мат и анти-спам** — чистый чат
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ❓ ВОПРОСЫ
|
||||||
|
|
||||||
|
**Не получается верифицироваться?**
|
||||||
|
→ Напиши админу в ЛС
|
||||||
|
|
||||||
|
**Бот не отвечает?**
|
||||||
|
→ Проверь, что ты верифицирован
|
||||||
|
|
||||||
|
**Нашёл баг?**
|
||||||
|
→ Сообщи админу
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**С уважением, администрация дома** 🏠
|
||||||
Loading…
Reference in a new issue