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_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:
raise ValueError('BOT_TOKEN не настроен! Проверьте .env файл')
@ -80,3 +107,19 @@ def get_admin_ids():
def is_admin(user_id: int) -> bool:
"""Проверка, является ли пользователь админом"""
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):
"""Рассылка текста"""
# Отправляем в чат дома
# Отправляем в чат дома в тему "Важные объявления"
try:
await bot.send_message(
config.ADMIN_CHAT_ID,
f'📢 <b>Объявление от администрации</b>\n\n'
f'{text}',
chat_id=config.ADMIN_CHAT_ID,
message_thread_id=config.get_topic_id('general'), # В общую тему
text=f'📢 <b>Объявление от администрации</b>\n\n{text}',
parse_mode='HTML'
)
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):
"""Рассылка картинки с подписью"""
# Отправляем в чат дома
# Отправляем в чат дома в тему "Важные объявления"
try:
await bot.send_photo(
config.ADMIN_CHAT_ID,
photo_id,
chat_id=config.ADMIN_CHAT_ID,
message_thread_id=config.get_topic_id('general'), # В общую тему
photo=photo_id,
caption=f'📢 <b>Объявление от администрации</b>\n\n{caption}',
parse_mode='HTML'
)

View file

@ -195,7 +195,7 @@ async def process_ads_phone(message: Message, state: FSMContext):
# Сохраняем объявление в БД
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:
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'🔖 ID: {ad.ad_id}'
# Отправляем в чат
# Отправляем в чат в тему "Важные объявления"
try:
sent_message = await message.bot.send_message(
config.ADMIN_CHAT_ID,
ad_text,
chat_id=config.ADMIN_CHAT_ID,
message_thread_id=config.get_topic_id('ads'), # Тема "Важные объявления"
text=ad_text,
parse_mode='HTML'
)

View file

@ -4,7 +4,7 @@ Initiative Group — хендлеры для работы с инициатив
"""
import logging
from aiogram import Router, F
from aiogram.types import Message
from aiogram.types import Message, CallbackQuery
from aiogram.filters import Command, StateFilter
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.context import FSMContext
@ -19,10 +19,108 @@ logger = logging.getLogger(__name__)
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 для добавления в ИГ
# =============================================================================
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):
waiting_for_notes = State()
@ -99,12 +197,170 @@ async def process_ig_notes(message: Message, state: FSMContext):
if notes and notes.lower() != 'пропустить':
async with AsyncSessionLocal() as 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 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_Оплатить можно через Госуслуги, Сбербанк или Тинькофф_'
# Отправляем в чат
# Отправляем в чат в тему "Важные объявления"
try:
await bot.send_message(
config.ADMIN_CHAT_ID,
text,
chat_id=config.ADMIN_CHAT_ID,
message_thread_id=config.get_topic_id('general'), # В общую тему
text=text,
parse_mode='HTML',
disable_web_page_preview=True
)

View file

@ -348,14 +348,15 @@ async def check_and_send_reminders(bot):
schedules = list(result.scalars().all())
for schedule in schedules:
# Отправляем напоминание в чат
# Отправляем напоминание в чат в тему "Важные объявления"
try:
emoji = schedule.get_type_emoji()
duration = (schedule.end_time - schedule.start_time).total_seconds() / 3600
await bot.send_message(
config.ADMIN_CHAT_ID,
f'⏰ <b>Напоминание: отключение через 1 час!</b>\n\n'
chat_id=config.ADMIN_CHAT_ID,
message_thread_id=config.get_topic_id('general'), # В общую тему
text=f'⏰ <b>Напоминание: отключение через 1 час!</b>\n\n'
f'{emoji} <b>{schedule.title}</b>\n\n'
f'📅 Начало: {schedule.start_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())
async def cmd_start(message: Message):
"""Команда /start (ТОЛЬКО В ЛИЧКЕ!)"""
from utils.ui_styles import get_level_badge
from aiogram.types import ReplyKeyboardMarkup, KeyboardButton
# Проверяем что это личка
if message.chat.type != 'private':
await message.answer(
@ -51,62 +54,213 @@ async def cmd_start(message: Message):
session.add(user)
await session.commit()
welcome_text = (
f'🏠 <b>Добро пожаловать, {message.from_user.first_name}!</b>\n\n'
f'Я — домовой бот нашего дома.\n\n'
f'❗ <b>Важно:</b> Для полного доступа пройдите верификацию.\n'
f'Отправьте мне номер вашей квартиры (например: <code>45</code> или <code>12А</code>)'
# 🔴 НОВОЕ СООБЩЕНИЕ С КНОПКАМИ (БЕЗ <font>!)
welcome_text = f'''🔴 <b>ДОБРО ПОЖАЛОВАТЬ</b>
👋 <b>Привет, {message.from_user.first_name}!</b>
Я домовой бот нашего дома.
🏠 <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:
# Обновляем имя
user.username = message.from_user.username
user.first_name = message.from_user.first_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:
# Проверяем членство в инициативной группе
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(
f'{header}\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'Выберите действие:',
reply_markup=get_main_menu(),
reply_markup=get_main_menu(verified=True, is_ig=is_ig, is_admin=is_admin, is_superadmin=is_superadmin),
parse_mode='HTML'
)
else:
# Неверифицированный — подробная инструкция (БЕЗ <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(
f'👋 Здравствуйте, {message.from_user.first_name}!\n\n'
f'⏳ Вы ещё не верифицированы.\n'
f'Отправьте номер квартиры для верификации.',
verify_text,
reply_markup=get_unverified_menu(),
parse_mode='HTML'
)
await session.commit()
@router.message(Command('помощь'))
@router.message(Command('help'))
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>
Просто отправьте номер квартиры текстом
Просто отправьте боту номер квартиры текстом
Примеры: <code>45</code>, <code>12А</code>, <code>100-1</code>
<b>Администратору:</b>
После верификации станут доступны:
📅 График отключений
💰 Платежи ЖКХ
🏆 Достижения
📋 Опросы
🔍 Моя квартира
'''
# Добавляем для админа
if level == 'admin':
text += '''
<b> Администратору:</b>
/admin Панель админа
/stats Статистика чата
/spies Подозрительные
/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')
@ -115,19 +269,35 @@ async def cb_help(callback: CallbackQuery):
await callback.answer()
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')
@ -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')
async def cb_phones_emergency(callback: CallbackQuery):
"""Экстренные телефоны"""
@ -338,12 +592,52 @@ async def cb_schedule(callback: CallbackQuery):
@router.callback_query(F.data == 'main_menu')
async def cb_main_menu(callback: CallbackQuery):
"""Главное меню"""
from utils.ui_styles import get_colored_header, get_level_badge
await callback.answer()
await callback.message.answer(
'🏠 <b>Главное меню</b>',
reply_markup=get_main_menu(),
parse_mode='HTML'
)
user_id = callback.from_user.id
async with AsyncSessionLocal() as session:
user = await session.get(User, user_id)
if user and user.verified:
# Проверяем ИГ и админа
from database.models import InitiativeGroup
ig_stmt = select(InitiativeGroup).where(
InitiativeGroup.user_id == user_id,
InitiativeGroup.is_active == True
)
ig_result = await session.execute(ig_stmt)
is_ig = ig_result.scalar_one_or_none() is not None
is_admin = user.is_admin or (user_id == config.ADMIN_USER_ID)
is_superadmin = (user_id == config.ADMIN_USER_ID)
# Определяем уровень
if is_superadmin:
level = 'superadmin'
header = get_colored_header('УПРАВЛЕНИЕ СИСТЕМОЙ', level)
elif is_admin:
level = 'admin'
header = get_colored_header('АДМИН-ПАНЕЛЬ', level)
elif is_ig:
level = 'ig'
header = get_colored_header('ИНИЦИАТИВНАЯ ГРУППА', level)
else:
level = 'verified'
header = get_colored_header('ГЛАВНОЕ МЕНЮ', level)
await callback.message.answer(
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(
'👷 Загружаю список мастеров...',
reply_markup=get_main_menu()
reply_markup=get_main_menu(verified=True)
)
# Перенаправляем на callback
async with AsyncSessionLocal() as session:
@ -628,13 +922,205 @@ async def cb_my_apartment_menu(callback: CallbackQuery):
await callback.message.answer('❌ Вы не зарегистрированы')
return
apt = user.apartment or 'Не указана'
verified = '' if user.verified else ''
apt = user.apartment or 'не указана'
phone = user.phone or 'не указан'
text = f'🏠 <b>Ваша квартира</b>\n\n'
text += f'Номер: <code>{apt}</code>\n'
text += f'Статус: {verified} {"Верифицирован" if user.verified else "Не верифицирован"}\n'
text += f'Рейтинг: {user.rating}\n'
text += f'Уровень: {user.level}\n'
text = '🔍 <b>Ваша квартира</b>\n\n'
text += f'🏠 Квартира: <code>{apt}</code>\n'
text += f'📞 Телефон: <code>{phone}</code>\n\n'
text += '<b>Хотите изменить?</b>\n\n'
text += 'Используйте команды:\n'
text += '<code>/setapt 45</code> — изменить квартиру\n'
text += '<code>/setphone +79991234567</code> — изменить телефон'
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.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.row(
InlineKeyboardButton(text='📞 Экстренные телефоны', callback_data='phones'),
InlineKeyboardButton(text=f'{level_emoji} 📞 Экстренные телефоны', callback_data='phones'),
)
builder.row(
InlineKeyboardButton(text='👷 Мастера', callback_data='masters'),
InlineKeyboardButton(text='👮 Участковый', callback_data='police'),
InlineKeyboardButton(text=f'{level_emoji} 👷 Мастера', callback_data='masters'),
)
# Только для верифицированных
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(
InlineKeyboardButton(text='📅 График отключений', callback_data='schedule'),
)
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'),
InlineKeyboardButton(text=f'{level_emoji} ❓ Помощь', callback_data='help'),
)
return builder.as_markup()
@ -147,6 +198,28 @@ def get_phones_menu() -> InlineKeyboardMarkup:
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:
"""Детали шпиона"""
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)
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:
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:
stmt = stmt.where((User.full_name.ilike(f"%{search}%")) |
(User.apartment.ilike(f"%{search}%")))
stmt = stmt.where(
(User.first_name.ilike(f"%{search}%")) |
(User.last_name.ilike(f"%{search}%")) |
(User.username.ilike(f"%{search}%")) |
(User.apartment.ilike(f"%{search}%"))
)
stmt = stmt.order_by(User.user_id.desc()).limit(100)
users = list((await session.execute(stmt)).scalars().all())
# Получаем список ИГ
from database.models import InitiativeGroup
ig_stmt = select(InitiativeGroup.user_id).where(InitiativeGroup.is_active == True)
ig_result = await session.execute(ig_stmt)
ig_user_ids = set(row[0] for row in ig_result.all())
# Статистика
total_count = (await session.execute(select(func.count(User.user_id)))).scalar()
verified_count = (await session.execute(select(func.count(User.user_id)).where(User.verified == True))).scalar()
unverified_count = total_count - verified_count
active_unverified = (await session.execute(
select(func.count(User.user_id)).where(User.verified == False, User.message_count >= 50)
)).scalar()
return templates.TemplateResponse("users.html",
{"request": request, "username": username, "users": users, "search": search,
"stats": {"total": total_count, "verified": verified_count, "unverified": total_count - verified_count, "active_unverified": active_unverified}})
{"request": request, "username": username, "users": users, "search": search, "current_filter": filter,
"stats": {"total": total_count, "verified": verified_count, "unverified": unverified_count, "active_unverified": active_unverified},
"ig_user_ids": ig_user_ids})
@app.post("/api/users/auto_verify")
@ -139,6 +163,31 @@ async def api_update_user(user_id: int, request: Request,
return JSONResponse({"success": True})
@app.get("/api/user/{user_id}")
async def api_get_user(user_id: int, username: str = Depends(get_current_admin)):
"""Получить данные пользователя"""
async with AsyncSessionLocal() as session:
user = await session.get(User, user_id)
if not user:
return JSONResponse({"error": "Пользователь не найден"}, status_code=404)
return JSONResponse({
"user": {
"id": user.user_id,
"username": user.username,
"first_name": user.first_name,
"last_name": user.last_name,
"apartment": user.apartment,
"phone": user.phone,
"verified": user.verified,
"rating": user.rating,
"message_count": user.message_count,
"is_banned": user.is_banned,
}
})
@app.post("/api/user/{user_id}/ban")
async def api_ban_user(user_id: int, username: str = Depends(get_current_admin)):
"""Забанить"""
@ -176,6 +225,54 @@ async def api_unverify_user(user_id: int, username: str = Depends(get_current_ad
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(
text: str = Form(...),
photo: UploadFile = File(None),
recipients: str = Form("all_and_chat"),
username: str = Depends(get_current_admin)
):
"""Отправить рассылку"""
import aiohttp
from pathlib import Path
from services.initiative_group import InitiativeGroupService
photo_id = None
# Если есть картинка — загружаем
if photo and photo.filename:
# Сохраняем временно
temp_path = Path("data") / f"broadcast_{photo.filename}"
with open(temp_path, "wb") as f:
content = await photo.read()
f.write(content)
# Загружаем в Telegram
try:
async with aiohttp.ClientSession() as session:
# Отправляем фото чтобы получить file_id
url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/sendPhoto"
data = aiohttp.FormData()
data.add_field('chat_id', config.ADMIN_USER_ID)
@ -425,16 +521,29 @@ async def api_send_broadcast(
except Exception as e:
logger.error(f"Ошибка загрузки фото: {e}")
# Рассылаем
# Получаем получателей
async with AsyncSessionLocal() as session:
stmt = select(User).where(User.verified == True)
result = await session.execute(stmt)
users = list(result.scalars().all())
if recipients == 'ig_only':
# Только ИГ
ig_service = InitiativeGroupService(session)
ig_user_ids = await ig_service.get_member_ids(active_only=True)
stmt = select(User).where(User.user_id.in_(ig_user_ids))
users = list((await session.execute(stmt)).scalars().all())
elif recipients == 'all_verified':
# Все верифицированные
stmt = select(User).where(User.verified == True)
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
error_count = 0
chat_sent = False
async with aiohttp.ClientSession() as http_session:
# Отправка пользователям
for user in users:
try:
url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/"
@ -459,9 +568,40 @@ async def api_send_broadcast(
except Exception as e:
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({
"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)
async def settings_page(request: Request, username: str = Depends(get_current_admin)):
"""Настройки"""

View file

@ -60,9 +60,20 @@
</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">
<i class="bi bi-info-circle"></i>
<b>Получатели:</b> Все верифицированные пользователи + общий чат
<b>Получатели:</b> <span id="recipientsInfo">Все верифицированные пользователи + общий чат</span>
</div>
<button type="submit" class="btn btn-primary btn-lg">
@ -91,6 +102,17 @@
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></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() {
const file = document.getElementById('broadcastPhoto').files[0];
const preview = document.getElementById('preview');
@ -113,6 +135,7 @@
const text = document.getElementById('broadcastText').value;
const photo = document.getElementById('broadcastPhoto').files[0];
const recipients = document.getElementById('broadcastRecipients').value;
const resultDiv = document.getElementById('result');
if (!text.trim()) {
@ -120,12 +143,20 @@
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>';
const formData = new FormData();
formData.append('text', text);
formData.append('recipients', recipients);
if (photo) {
formData.append('photo', photo);
}

View file

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

View file

@ -2,15 +2,11 @@
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
.sidebar {
min-height: 100vh;
background: linear-gradient(180deg, #2c3e50 0%, #1a252f 100%);
}
.sidebar { 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:hover, .sidebar .nav-link.active { color: white; background-color: rgba(255,255,255,0.1); }
.content { padding: 2rem; }
@ -26,54 +22,82 @@
<nav class="nav flex-column">
<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="/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" href="/export"><i class="bi bi-download"></i> Экспорт</a>
</nav>
</div>
<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">
<div class="card-header">
<i class="bi bi-info-circle"></i> Текущая конфигурация
<!-- IG Broadcast Limit -->
<div class="card mb-4">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="bi bi-people"></i> Рассылка Инициативной Группы</h5>
</div>
<div class="card-body">
<table class="table">
<tr>
<th>Bot Token:</th>
<td><code>{{ config.bot_token }}</code></td>
</tr>
<tr>
<th>Admin Chat ID:</th>
<td><code>{{ config.admin_chat_id }}</code></td>
</tr>
<tr>
<th>Admin User ID:</th>
<td><code>{{ config.admin_user_id }}</code></td>
</tr>
<tr>
<th>Прокси:</th>
<td>
{% if config.use_proxy %}
<span class="badge bg-success">✅ Включен</span>
{% else %}
<span class="badge bg-danger">❌ Отключен</span>
{% endif %}
</td>
</tr>
</table>
<form id="igBroadcastForm">
<div class="mb-3">
<label class="form-label">Минимальный интервал между рассылками (секунды)</label>
<input type="number" class="form-control" id="igBroadcastLimit" value="{{ settings.ig_broadcast_limit }}" min="0" step="60">
<small class="text-muted">
<ul class="mb-0">
<li><code>0</code> — Без ограничений (можно спамить!)</li>
<li><code>3600</code> — 1 раз в час</li>
<li><code>86400</code> — 1 раз в сутки</li>
</ul>
</small>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-save"></i> Сохранить
</button>
</form>
<div id="igBroadcastResult" class="mt-3"></div>
</div>
</div>
<div class="alert alert-info mt-4">
<i class="bi bi-info-circle"></i>
Для изменения настроек отредактируйте файл <code>.env</code> и перезапустите бота.
<!-- Info -->
<div class="card">
<div class="card-body">
<h5> Информация</h5>
<p>Здесь будут настройки бота для суперадмина.</p>
</div>
</div>
</div>
</div>
</div>
<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>
</html>

View file

@ -34,42 +34,68 @@
</div>
<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 -->
{% if stats %}
<div class="row mb-4">
<div class="col-md-3">
<div class="card bg-primary text-white">
<div class="card-body text-center">
<h3>{{ stats.total }}</h3>
<small>Всего пользователей</small>
<a href="/users" style="text-decoration: none;">
<div class="card bg-primary text-white">
<div class="card-body text-center">
<h3>{{ stats.total }}</h3>
<small>Всего пользователей</small>
</div>
</div>
</div>
</a>
</div>
<div class="col-md-3">
<div class="card bg-success text-white">
<div class="card-body text-center">
<h3>{{ stats.verified }}</h3>
<small>Верифицировано</small>
<a href="/users?filter=verified" style="text-decoration: none;">
<div class="card bg-success text-white">
<div class="card-body text-center">
<h3>{{ stats.verified }}</h3>
<small>Верифицировано</small>
</div>
</div>
</div>
</a>
</div>
<div class="col-md-3">
<div class="card bg-warning text-dark">
<div class="card-body text-center">
<h3>{{ stats.unverified }}</h3>
<small>Не верифицировано</small>
<a href="/users?filter=unverified" style="text-decoration: none;">
<div class="card bg-warning text-dark">
<div class="card-body text-center">
<h3>{{ stats.unverified }}</h3>
<small>Не верифицировано</small>
</div>
</div>
</div>
</a>
</div>
<div class="col-md-3">
<div class="card bg-info text-white">
<div class="card-body text-center">
<h3>{{ stats.active_unverified }}</h3>
<small>Активных (50+ сообщений)</small>
<a href="/users?filter=active_unverified" style="text-decoration: none;">
<div class="card bg-info text-white">
<div class="card-body text-center">
<h3>{{ stats.active_unverified }}</h3>
<small>Активных (50+ сообщений)</small>
</div>
</div>
</div>
</a>
</div>
</div>
{% endif %}
@ -113,6 +139,43 @@
<!-- Users Table -->
<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">
<table class="table table-hover">
<thead>
@ -130,6 +193,7 @@
</thead>
<tbody>
{% for user in users %}
{% set is_in_ig = user.user_id in ig_user_ids %}
<tr>
<td>{{ user.user_id }}</td>
<td>{{ user.full_name or '—' }}</td>
@ -177,6 +241,24 @@
<i class="bi bi-shield-check"></i>
</button>
{% 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>
</tr>
{% 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() {
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
**Расписание отключений** — будь в курсе
**Рейтинг активности** — стань лидером дома!
**Достижения** — получай за активность
**Детект шпионов** — защита от ботов
**Анти-мат и анти-спам** — чистый чат
---
## ❓ ВОПРОСЫ
**Не получается верифицироваться?**
→ Напиши админу в ЛС
**Бот не отвечает?**
→ Проверь, что ты верифицирован
**Нашёл баг?**
→ Сообщи админу
---
**С уважением, администрация дома** 🏠