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:
Admin 2026-03-10 17:01:30 +00:00
parent da010eccbf
commit cef2ac3714
24 changed files with 3568 additions and 220 deletions

296
ACCESS_LEVELS_TABLE.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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** 🏠
**Полное управление пользователями в веб-панели!** 🖥️✅

View file

@ -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)

View file

@ -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'
) )

View file

@ -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'
) )

View file

@ -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'
)
# ============================================================================= # =============================================================================
# УДАЛЕНИЕ ИЗ ИГ # УДАЛЕНИЕ ИЗ ИГ
# ============================================================================= # =============================================================================

View file

@ -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
) )

View file

@ -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'

View file

@ -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,12 +592,52 @@ 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()
await callback.message.answer(
'🏠 <b>Главное меню</b>', user_id = callback.from_user.id
reply_markup=get_main_menu(), async with AsyncSessionLocal() as session:
parse_mode='HTML' 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(
f'{header}\n\n{get_level_badge(level)}',
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'
)
# Текстовые команды (алиасы) # Текстовые команды (алиасы)
@ -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 &lt;user_id&gt; [квартира] [телефон]</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
View 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())

View file

@ -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(
InlineKeyboardButton(text=f'{level_emoji} 🏆 Достижения', callback_data='achievements'),
InlineKeyboardButton(text=f'{level_emoji} 📅 События', callback_data='events'),
)
builder.row(
InlineKeyboardButton(text=f'{level_emoji} 💰 Платежи', callback_data='payments'),
InlineKeyboardButton(text=f'{level_emoji} 📋 Опросы', callback_data='polls'),
)
builder.row(
InlineKeyboardButton(text=f'{level_emoji} 🔍 Моя квартира', callback_data='my_apartment'),
)
builder.row(
InlineKeyboardButton(text=f'{level_emoji} 🙏 Поблагодарить', callback_data='thank_user'),
)
# Для членов инициативной группы — дополнительные кнопки
if is_ig:
builder.row(
InlineKeyboardButton(text=f'{level_emoji} 🎯 Инициативная группа', callback_data='ig_menu'),
)
# Для админов — кнопка админки
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( builder.row(
InlineKeyboardButton(text='📅 График отключений', callback_data='schedule'), InlineKeyboardButton(text=f'{level_emoji} ❓ Помощь', callback_data='help'),
)
builder.row(
InlineKeyboardButton(text='🏆 Достижения', callback_data='achievements'),
InlineKeyboardButton(text='📅 События', callback_data='events'),
)
builder.row(
InlineKeyboardButton(text='💰 Платежи', callback_data='payments'),
InlineKeyboardButton(text='📋 Опросы', callback_data='polls'),
)
builder.row(
InlineKeyboardButton(text='🔍 Моя квартира', callback_data='my_apartment'),
)
builder.row(
InlineKeyboardButton(text='🙏 Поблагодарить', callback_data='thank_user'),
InlineKeyboardButton(text='❓ Помощь', 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
View 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'

View file

@ -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:
stmt = select(User).where(User.verified == True) if recipients == 'ig_only':
result = await session.execute(stmt) # Только ИГ
users = list(result.scalars().all()) 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)
users = list((await session.execute(stmt)).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)):
"""Настройки""" """Настройки"""

View file

@ -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);
} }

View file

@ -122,28 +122,36 @@
<!-- Stats Cards --> <!-- Stats Cards -->
<div class="row"> <div class="row">
<div class="col-md-3"> <div class="col-md-3">
<div class="stat-card primary"> <a href="/users?filter=all" style="text-decoration: none;">
<div class="stat-value">{{ stats.users }}</div> <div class="stat-card primary">
<div class="stat-label">Пользователей</div> <div class="stat-value">{{ stats.users }}</div>
</div> <div class="stat-label">Пользователей</div>
</div>
</a>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="stat-card success"> <a href="/users?filter=verified" style="text-decoration: none;">
<div class="stat-value">{{ stats.verified }}</div> <div class="stat-card success">
<div class="stat-label">Верифицировано</div> <div class="stat-value">{{ stats.verified }}</div>
</div> <div class="stat-label">Верифицировано</div>
</div>
</a>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="stat-card warning"> <a href="/users?filter=unverified" style="text-decoration: none;">
<div class="stat-value">{{ stats.messages }}</div> <div class="stat-card warning">
<div class="stat-label">Сообщений</div> <div class="stat-value">{{ stats.unverified }}</div>
</div> <div class="stat-label">Не верифицировано</div>
</div>
</a>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="stat-card danger"> <a href="/users?filter=active_unverified" style="text-decoration: none;">
<div class="stat-value">{{ stats.polls }}</div> <div class="stat-card danger">
<div class="stat-label">Активных опросов</div> <div class="stat-value">{{ stats.active_unverified }}</div>
</div> <div class="stat-label">Активных (50+ сообщ.)</div>
</div>
</a>
</div> </div>
</div> </div>

View file

@ -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>

View file

@ -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">
<div class="card bg-primary text-white"> <a href="/users" style="text-decoration: none;">
<div class="card-body text-center"> <div class="card bg-primary text-white">
<h3>{{ stats.total }}</h3> <div class="card-body text-center">
<small>Всего пользователей</small> <h3>{{ stats.total }}</h3>
<small>Всего пользователей</small>
</div>
</div> </div>
</div> </a>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="card bg-success text-white"> <a href="/users?filter=verified" style="text-decoration: none;">
<div class="card-body text-center"> <div class="card bg-success text-white">
<h3>{{ stats.verified }}</h3> <div class="card-body text-center">
<small>Верифицировано</small> <h3>{{ stats.verified }}</h3>
<small>Верифицировано</small>
</div>
</div> </div>
</div> </a>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="card bg-warning text-dark"> <a href="/users?filter=unverified" style="text-decoration: none;">
<div class="card-body text-center"> <div class="card bg-warning text-dark">
<h3>{{ stats.unverified }}</h3> <div class="card-body text-center">
<small>Не верифицировано</small> <h3>{{ stats.unverified }}</h3>
<small>Не верифицировано</small>
</div>
</div> </div>
</div> </a>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="card bg-info text-white"> <a href="/users?filter=active_unverified" style="text-decoration: none;">
<div class="card-body text-center"> <div class="card bg-info text-white">
<h3>{{ stats.active_unverified }}</h3> <div class="card-body text-center">
<small>Активных (50+ сообщений)</small> <h3>{{ stats.active_unverified }}</h3>
<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;

View 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
**Расписание отключений** — будь в курсе
**Рейтинг активности** — стань лидером дома!
**Достижения** — получай за активность
**Детект шпионов** — защита от ботов
**Анти-мат и анти-спам** — чистый чат
---
## ❓ ВОПРОСЫ
**Не получается верифицироваться?**
→ Напиши админу в ЛС
**Бот не отвечает?**
→ Проверь, что ты верифицирован
**Нашёл баг?**
→ Сообщи админу
---
**С уважением, администрация дома** 🏠