diff --git a/ACCESS_LEVELS_TABLE.md b/ACCESS_LEVELS_TABLE.md new file mode 100644 index 0000000..07b6eb6 --- /dev/null +++ b/ACCESS_LEVELS_TABLE.md @@ -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 ` | +| **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 member +2. Жилец в ИГ ✅ +``` + +### Вериф. → Админ +``` +1. В базе: UPDATE users SET is_admin=True WHERE user_id= +2. Или в .env: ADMIN_USER_ID= +``` + +--- + +## 💡 ЗАПОМНИ + +| Уровень | Кнопки | Команды | Веб-админка | +|---------|--------|---------|-------------| +| **Невериф.** | 5 | 3 | ❌ | +| **Вериф.** | 12 | 10 | ❌ | +| **ИГ** | 14 | 16 | ❌ | +| **Админ** | 16 | 30+ | ✅ | +| **Супер-админ** | 16 | 30+ | ✅ + сервер | + +--- + +**Спокойной ночи, бро!** 🌙 +**Бот под надёжным контролем!** 🏠 diff --git a/COLOR_MENU_SYSTEM.md b/COLOR_MENU_SYSTEM.md new file mode 100644 index 0000000..21b9e8a --- /dev/null +++ b/COLOR_MENU_SYSTEM.md @@ -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** 🏠 +**Цветной бот — красивый бот!** 🎨 diff --git a/EDIT_USER_FEATURE.md b/EDIT_USER_FEATURE.md new file mode 100644 index 0000000..b54d853 --- /dev/null +++ b/EDIT_USER_FEATURE.md @@ -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 [квартира] [телефон] +``` + +### Примеры: + +**Установить квартиру и телефон:** +```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** 🏠 +**Теперь редактирование профилей доступно везде!** 📱🖥️ diff --git a/FORUM_TOPICS_SETUP.md b/FORUM_TOPICS_SETUP.md new file mode 100644 index 0000000..7e99440 --- /dev/null +++ b/FORUM_TOPICS_SETUP.md @@ -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** 🏠 diff --git a/INITIATIVE_GROUP_GUIDE.md b/INITIATIVE_GROUP_GUIDE.md new file mode 100644 index 0000000..78b9e9e --- /dev/null +++ b/INITIATIVE_GROUP_GUIDE.md @@ -0,0 +1,235 @@ +# 🎯 ИНИЦИАТИВНАЯ ГРУППА — ИНСТРУКЦИЯ + +**Обновлено:** 5 марта 2026 +**Статус:** ✅ РЕАЛИЗОВАНО + +--- + +## 📋 ЧТО ТАКОЕ ИНИЦИАТИВНАЯ ГРУППА (ИГ)? + +**Инициативная группа** — это активные жильцы, которые помогают управлять домом. + +### Уровни доступа: + +| Уровень | Доступ | +|---------|--------| +| **Неверифицированный** | Только телефоны + правила | +| **Верифицированный** | Все функции бота | +| **Инициативная группа** | Всё + меню ИГ | +| **Админ** | Всё + админ-панель | + +--- + +## ✅ НАЗНАЧЕНИЕ В ИГ + +### Можешь ли назначать НЕзависимо от верификации? + +**ДА!** ✅ Верификация **НЕ требуется** для вступления в ИГ! + +### Как назначить в ИГ: + +```bash +# Добавить пользователя в ИГ +/ig_add [роль] + +# Роли: +# - 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 [роль] +``` + +### Удалить из ИГ: +```bash +/ig_remove +``` + +### Список участников ИГ: +```bash +/ig_list +``` + +### Статистика ИГ: +```bash +/ig_stats +``` + +### Проверить участника: +```bash +/ig_check +``` + +### Рассылка только ИГ: +```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** 🏠 diff --git a/PROFILE_FEATURE.md b/PROFILE_FEATURE.md new file mode 100644 index 0000000..9969a79 --- /dev/null +++ b/PROFILE_FEATURE.md @@ -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** 🏠 diff --git a/UX_UPDATE_v1.15.md b/UX_UPDATE_v1.15.md new file mode 100644 index 0000000..8d1ebe9 --- /dev/null +++ b/UX_UPDATE_v1.15.md @@ -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** 🏠 diff --git a/WEB_USER_MANAGEMENT.md b/WEB_USER_MANAGEMENT.md new file mode 100644 index 0000000..42dc4be --- /dev/null +++ b/WEB_USER_MANAGEMENT.md @@ -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** 🏠 +**Полное управление пользователями в веб-панели!** 🖥️✅ diff --git a/config.py b/config.py index b7feb42..e2f0686 100644 --- a/config.py +++ b/config.py @@ -41,6 +41,33 @@ WEB_ADMIN_PASSWORD = os.getenv('WEB_ADMIN_PASSWORD', 'admin') WEB_ADMIN_HOST = os.getenv('WEB_ADMIN_HOST', '0.0.0.0') WEB_ADMIN_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) diff --git a/handlers/admin.py b/handlers/admin.py index 576d6fc..ece72fe 100644 --- a/handlers/admin.py +++ b/handlers/admin.py @@ -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'📢 Объявление от администрации\n\n' - f'{text}', + chat_id=config.ADMIN_CHAT_ID, + message_thread_id=config.get_topic_id('general'), # В общую тему + text=f'📢 Объявление от администрации\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'📢 Объявление от администрации\n\n{caption}', parse_mode='HTML' ) diff --git a/handlers/ads.py b/handlers/ads.py index 92c5969..2a5e29c 100644 --- a/handlers/ads.py +++ b/handlers/ads.py @@ -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( @@ -227,19 +227,20 @@ 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' ) - + # Сохраняем message_id ad.message_id = sent_message.message_id await session.commit() - + except Exception as e: logger.error(f'Не удалось опубликовать объявление: {e}') diff --git a/handlers/initiative_group.py b/handlers/initiative_group.py index 7dd7ea0..8c9ec56 100644 --- a/handlers/initiative_group.py +++ b/handlers/initiative_group.py @@ -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 += 'Ваши возможности:\n' + text += '• 📊 Статистика активности\n' + text += '• 📋 Список участников\n' + text += '• 👤 Мой профиль\n' + text += '• 📣 Рассылка для ИГ\n' + text += '• 📢 Обращения к администрации\n' + text += '• 🗳️ Голосования ИГ\n\n' + text += 'Больше функций в разработке...' + + 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( + 'ℹ️ Инициативная группа\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( + '📣 Рассылка для Инициативной Группы\n\n' + 'Отправьте текст сообщения:\n\n' + 'Или прикрепите файл (PDF, фото и т.д.)\n\n' + 'Отправьте "отмена" для выхода.', + parse_mode='HTML' + ) + await callback.from_user.id + await callback.message.bot.get_updates() + await callback.message.answer('👇 Отправьте текст или файл:') + # Проще через команду + await callback.message.answer( + 'ℹ️ Для отправки рассылки используйте команду:\n\n' + '/ig_broadcast\n\n' + 'Бот попросит ввести текст и файл.', + parse_mode='HTML' + ) + class IGState(StatesGroup): waiting_for_notes = State() @@ -93,18 +191,176 @@ async def process_ig_notes(message: Message, state: FSMContext): """Получили заметку о роли""" data = await state.get_data() user_id = data.get('ig_user_id') - + notes = message.text if message.text.lower() != 'пропустить' else None - + 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 = '📊 Ваша статистика в ИГ\n\n' + text += f'👤 {user.full_name or user.username}\n' + text += f'🏠 Квартира: {user.apartment or "не указана"}\n\n' + text += f'🎭 Роль: {member_info["role"]}\n' + text += f'📅 В группе с: {member_info["joined_at"].strftime("%d.%m.%Y")}\n\n' + text += f'📊 Ваша активность:\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 = '📋 Участники инициативной группы\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{role_names.get(role, role)}\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'📞 Телефон: {user.phone or "не указан"}\n' + apt_text = f'🏠 Квартира: {user.apartment or "не указана"}\n' + + text = '👤 Ваш профиль\n\n' + text += apt_text + text += phone_text + text += '\nЧто хотите изменить?' + + 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( + '🏠 Изменение квартиры\n\n' + 'ℹ️ Используйте команду:\n\n' + '/setapt 45\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( + '📞 Изменение телефона\n\n' + 'ℹ️ Используйте команду:\n\n' + '/setphone +79991234567\n\n' + 'Или нажмите "Назад".', + parse_mode='HTML' + ) + + # ============================================================================= # УДАЛЕНИЕ ИЗ ИГ # ============================================================================= diff --git a/handlers/payments.py b/handlers/payments.py index b6ed152..8577a16 100644 --- a/handlers/payments.py +++ b/handlers/payments.py @@ -371,23 +371,24 @@ async def send_payment_reminders(bot): pass 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 ) - + # Обновляем last_reminder_sent for r in reminders: r.last_reminder_sent = datetime.utcnow() - + await session.commit() - + logger.info(f'Напоминания о платежах отправлены ({len(reminders)} платежей)') - + except Exception as e: logger.error(f'Не удалось отправить напоминания о платежах: {e}') diff --git a/handlers/schedule.py b/handlers/schedule.py index 92873d2..73f39d4 100644 --- a/handlers/schedule.py +++ b/handlers/schedule.py @@ -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'⏰ Напоминание: отключение через 1 час!\n\n' + chat_id=config.ADMIN_CHAT_ID, + message_thread_id=config.get_topic_id('general'), # В общую тему + text=f'⏰ Напоминание: отключение через 1 час!\n\n' f'{emoji} {schedule.title}\n\n' f'📅 Начало: {schedule.start_time.strftime("%d.%m.%Y %H:%M")}\n' f'📅 Окончание: {schedule.end_time.strftime("%d.%m.%Y %H:%M")}\n' @@ -363,12 +364,12 @@ async def check_and_send_reminders(bot): f'Не забудьте сделать запасы!', parse_mode='HTML' ) - + # Помечаем что напоминание отправлено schedule.reminder_sent = True await session.commit() - + logger.info(f'Напоминание об отключении "{schedule.title}" отправлено') - + except Exception as e: logger.error(f'Не удалось отправить напоминание: {e}') diff --git a/handlers/users.py b/handlers/users.py index f8232a1..8ac545a 100644 --- a/handlers/users.py +++ b/handlers/users.py @@ -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( @@ -31,7 +34,7 @@ async def cmd_start(message: Message): parse_mode='HTML' ) return - + user_id = message.from_user.id async with AsyncSessionLocal() as session: @@ -51,62 +54,213 @@ async def cmd_start(message: Message): session.add(user) await session.commit() - welcome_text = ( - f'🏠 Добро пожаловать, {message.from_user.first_name}!\n\n' - f'Я — домовой бот нашего дома.\n\n' - f'❗ Важно: Для полного доступа пройдите верификацию.\n' - f'Отправьте мне номер вашей квартиры (например: 45 или 12А)' + # 🔴 НОВОЕ СООБЩЕНИЕ С КНОПКАМИ (БЕЗ !) + welcome_text = f'''🔴 ДОБРО ПОЖАЛОВАТЬ + +👋 Привет, {message.from_user.first_name}! + +Я — домовой бот нашего дома. + +🏠 Что я умею: +• 📞 Телефоны экстренных служб +• 👷 Проверенные мастера +• 📅 График отключений +• 💰 Напоминания о платежах +• 🏆 Рейтинг и достижения +• 📋 Опросы и голосования + +⚠️ ВАЖНО: +Для полного доступа пройдите верификацию! + +📝 Как пройти верификацию: + +Шаг 1: Отправьте номер квартиры +Примеры: 45, 12А, 100-1 + +Шаг 2: (опционально) Отправьте телефон +Пример: +79991234567 + +Шаг 3: Дождитесь подтверждения админа + +После верификации откроются все функции! 🎉''' + + # Кнопки для быстрого старта + 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 = '🟣 УПРАВЛЕНИЕ СИСТЕМОЙ' + elif is_admin: + level = 'admin' + header = '🟡 АДМИН-ПАНЕЛЬ' + elif is_ig: + level = 'ig' + header = '🔵 ИНИЦИАТИВНАЯ ГРУППА' + else: + level = 'verified' + header = '🟢 ДОСТУП РАЗРЕШЁН' + 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: + # Неверифицированный — подробная инструкция (БЕЗ !) + verify_text = f'''🔴 ТРЕБУЕТСЯ ВЕРИФИКАЦИЯ + +👋 Здравствуйте, {message.from_user.first_name}! + +{get_level_badge("unverified")} + +⏳ Вы ещё не верифицированы + +📝 Что делать? + +1️⃣ Отправьте номер квартиры +Примеры: +45 +12А +100-1 + +2️⃣ (опционально) Телефон +Пример: ++79991234567 + +3️⃣ Дождитесь подтверждения +Админ проверит и подтвердит. + +✅ После верификации получите доступ ко всем функциям! + +💡 Нужна помощь? +Напишите администратору чата.''' + 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 = '''📱 Команды бота: + """Справка по командам (для жильцов)""" + from utils.ui_styles import get_colored_header, get_level_badge, get_user_level_with_ig + from database.models import InitiativeGroup + + 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)} + +📱 Доступные команды: + +Для всех: 📞 /телефон — Экстренные службы 👷 /мастера — Сантехники, электрики 👮 /участковый — Контакты полиции -📅 /график — Отключения воды/света -📢 /объявления — Новости дома -🔍 /моя_квартира — Проверка привязки +📜 /правила — Правила чата +❓ /помощь — Эта справка Верификация: -Просто отправьте номер квартиры текстом +Просто отправьте боту номер квартиры текстом +Примеры: 45, 12А, 100-1 -Администратору: +После верификации станут доступны: +• 📅 График отключений +• 💰 Платежи ЖКХ +• 🏆 Достижения +• 📋 Опросы +• 🔍 Моя квартира +''' + + # Добавляем для админа + if level == 'admin': + text += ''' +⚙️ Администратору: /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 += ''' +🎯 Инициативной группе: +/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,20 +269,36 @@ async def cb_help(callback: CallbackQuery): await callback.answer() text = '''🏠 Домовой бот — помощь -Этот бот создан для помощи жильцам нашего дома. +Этот бот помогает жильцам нашего дома быть в курсе событий и решать бытовые вопросы. -📞 Экстренные телефоны -Быстрый доступ к телефонам служб +🆕 Вы впервые здесь? -👷 Мастера -Проверенные сантехники и электрики +1️⃣ Пройдите верификацию — это просто! + • Отправьте боту номер вашей квартиры + • Например: 45 или 12А + • После проверки вы получите полный доступ -🔍 Моя квартира -Проверить привязку к квартире +2️⃣ Что будет после верификации? + • 📅 График отключений воды и света + • 📢 Объявления и новости дома + • 💰 Напоминания о платежах ЖКХ + • 🏆 Рейтинг и достижения + • 📋 Участие в опросах + • 📅 События и собрания + +📞 Телефоны служб +Нажмите "Экстренные телефоны" чтобы увидеть: +• 🚨 Аварийные службы (02, 03, 04, 112) +• 🔧 ЖКХ, сантехники, электрики +• 👮 Участковый полиция + +❓ Нужна помощь? +Напишите администратору чата или используйте команду /help + +🔐 Конфиденциальность +Бот видит только ваши сообщения в этом чате. +Доступ к данным есть только у администраторов.''' -⏳ Верификация -Отправьте номер квартиры текстом''' - 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 = '''📜 Правила нашего чата + +1️⃣ Уважайте соседей + • Не оскорбляйте и не унижайте других + • Запрещены угрозы и дискриминация + • Критикуйте идеи, а не людей + +2️⃣ Не спамьте + • Максимум 10 сообщений в минуту + • Запрещена реклама без согласования + • Не отправляйте цепочки и фейки + +3️⃣ Мат — под запретом + • 3 предупреждения = бан + • Используйте цензурную лексику + +4️⃣ Темы для обсуждений + • Вопросы по дому и ЖКХ + • Соседи и взаимопомощь + • Объявления (продам, куплю, отдам) + • События и собрания + +5️⃣ Верификация обязательна + • Отправьте номер квартиры боту + • Без верификации доступ ограничен + • Это защита от шпионов и ботов + +⚠️ Нарушения +• Предупреждение (устно) +• Мут на 30 минут +• Бан навсегда + +💡 Хотите помочь чату? +Вступайте в инициативную группу!''' + + await callback.message.answer(text, parse_mode='HTML') + + +@router.message(Command('правила')) +@router.message(Command('rules')) +async def cmd_rules(message: Message): + """Правила чата (команда)""" + text = '''📜 Правила нашего чата + +1️⃣ Уважайте соседей + • Не оскорбляйте и не унижайте других + • Запрещены угрозы и дискриминация + • Критикуйте идеи, а не людей + +2️⃣ Не спамьте + • Максимум 10 сообщений в минуту + • Запрещена реклама без согласования + • Не отправляйте цепочки и фейки + +3️⃣ Мат — под запретом + • 3 предупреждения = бан + • Используйте цензурную лексику + +4️⃣ Темы для обсуждений + • Вопросы по дому и ЖКХ + • Соседи и взаимопомощь + • Объявления (продам, куплю, отдам) + • События и собрания + +5️⃣ Верификация обязательна + • Отправьте номер квартиры боту + • Без верификации доступ ограничен + • Это защита от шпионов и ботов + +⚠️ Нарушения +• Предупреждение (устно) +• Мут на 30 минут +• Бан навсегда + +💡 Хотите помочь чату? +Вступайте в инициативную группу!''' + + 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( - '🏠 Главное меню', - 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: @@ -618,23 +912,215 @@ async def cb_schedule_menu(callback: CallbackQuery): async def cb_my_apartment_menu(callback: CallbackQuery): """Кнопка Моя квартира""" await callback.answer() - + async with AsyncSessionLocal() as session: stmt = select(User).where(User.user_id == callback.from_user.id) result = await session.execute(stmt) user = result.scalar_one_or_none() - + if not user: await callback.message.answer('❌ Вы не зарегистрированы') return - - apt = user.apartment or '❓ Не указана' - verified = '✅' if user.verified else '⏳' - - text = f'🏠 Ваша квартира\n\n' - text += f'Номер: {apt}\n' - text += f'Статус: {verified} {"Верифицирован" if user.verified else "Не верифицирован"}\n' - text += f'Рейтинг: {user.rating}\n' - text += f'Уровень: {user.level}\n' + + apt = user.apartment or 'не указана' + phone = user.phone or 'не указан' + + text = '🔍 Ваша квартира\n\n' + text += f'🏠 Квартира: {apt}\n' + text += f'📞 Телефон: {phone}\n\n' + text += 'Хотите изменить?\n\n' + text += 'Используйте команды:\n' + text += '/setapt 45 — изменить квартиру\n' + text += '/setphone +79991234567 — изменить телефон' 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( + '❌ Использование:\n' + '/setapt 45\n\n' + 'Где 45 — ваш номер квартиры.', + parse_mode='HTML' + ) + return + + apartment = args[1].strip().upper() + + if not validate_apartment(apartment): + await message.answer( + '❌ Неверный формат квартиры.\n\n' + 'Примеры: 45, 12А, 100-1', + 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'✅ Квартира обновлена!\n\n' + f'Ваша квартира: {apartment}', + 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( + '❌ Использование:\n' + '/setphone +79991234567', + parse_mode='HTML' + ) + return + + phone = args[1].strip() + + # Простая валидация + if not phone.startswith('+') or len(phone) < 10: + await message.answer( + '❌ Неверный формат телефона.\n\n' + 'Пример: +79991234567', + 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'✅ Телефон обновлён!\n\n' + f'Ваш телефон: {phone}', + parse_mode='HTML' + ) + + +# ============================================================================= +# РЕДАКТИРОВАНИЕ ПРОФИЛЯ ЖИЛЬЦА (ДЛЯ АДМИНА) +# ============================================================================= + +@router.message(Command('edit_user')) +@router.message(Command('edituser')) +async def cmd_edit_user(message: Message): + """ + Редактировать профиль жильца (для админа) + Использование: /edit_user [квартира] [телефон] + """ + if not config.is_admin(message.from_user.id): + await message.answer('❌ Только для администраторов') + return + + args = message.text.split() + + if len(args) < 2: + await message.answer( + '❌ Использование:\n\n' + '/edit_user <user_id> [квартира] [телефон]\n\n' + 'Примеры:\n' + '/edit_user 123456789 45 +79991234567\n' + '/edit_user 123456789 apt:45 phone:+79991234567\n' + '/edit_user 123456789 phone:+79991234567\n' + '/edit_user 123456789 apt:45', + 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' + 'Пример: /edit_user 123456789 45 +79991234567', + parse_mode='HTML' + ) + return + + await session.commit() + + name = user.full_name or user.username or f'user{user_id}' + + await message.answer( + f'✅ Профиль обновлён!\n\n' + f'👤 {name}\n' + f'🔖 ID: {user_id}\n\n' + f'Обновлено:\n' + + '\n'.join(f'• {item}' for item in updated), + parse_mode='HTML' + ) diff --git a/import_members.py b/import_members.py new file mode 100644 index 0000000..e9d0009 --- /dev/null +++ b/import_members.py @@ -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()) diff --git a/keyboards/inline.py b/keyboards/inline.py index d71595f..de74e59 100644 --- a/keyboards/inline.py +++ b/keyboards/inline.py @@ -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() @@ -131,7 +182,7 @@ def get_verification_menu() -> InlineKeyboardMarkup: def get_phones_menu() -> InlineKeyboardMarkup: """Меню телефонов""" builder = InlineKeyboardBuilder() - + builder.row( InlineKeyboardButton(text='🚨 Экстренные', callback_data='phones_emergency'), InlineKeyboardButton(text='🔧 ЖКХ', callback_data='phones_utility'), @@ -143,7 +194,29 @@ def get_phones_menu() -> InlineKeyboardMarkup: builder.row( InlineKeyboardButton(text='🔙 Назад', callback_data='main_menu'), ) - + + 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() diff --git a/utils/ui_styles.py b/utils/ui_styles.py new file mode 100644 index 0000000..45e971c --- /dev/null +++ b/utils/ui_styles.py @@ -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 (без тегов!) + + Args: + text: текст заголовка + level: уровень пользователя + + Returns: + строка с emoji и жирным текстом + """ + emoji = LEVEL_EMOJI.get(level, '⚪') + + return f'{emoji} {text}' + + +def get_level_badge(level: str) -> str: + """ + Получить бейдж уровня (без HTML тегов!) + + Args: + level: уровень пользователя + + Returns: + строка с emoji и названием + """ + emoji = LEVEL_EMOJI.get(level, '⚪') + name = LEVEL_NAMES.get(level, 'НЕИЗВЕСТНО') + + return f'{emoji} {name}' + + +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' diff --git a/web/app.py b/web/app.py index 4db8a0c..0243244 100644 --- a/web/app.py +++ b/web/app.py @@ -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") @@ -120,7 +144,7 @@ async def api_auto_verify_users(username: str = Depends(get_current_admin)): @app.post("/api/user/{user_id}/update") -async def api_update_user(user_id: int, request: Request, +async def api_update_user(user_id: int, request: Request, username: str = Depends(get_current_admin)): """Обновить пользователя""" data = await request.json() @@ -128,7 +152,7 @@ async def api_update_user(user_id: int, request: Request, update_data = {} if 'apartment' in data: update_data['apartment'] = data['apartment'].upper() if 'phone' in data: update_data['phone'] = data['phone'] - if 'verified' in data: + if 'verified' in data: update_data['verified'] = bool(data['verified']) if data['verified']: update_data['verification_date'] = datetime.utcnow() if 'banned' in data: update_data['is_banned'] = bool(data['banned']) @@ -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,49 +489,61 @@ 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) data.add_field('photo', open(temp_path, 'rb'), filename=photo.filename) data.add_field('caption', 'Test') - + async with session.post(url, data=data) as resp: result = await resp.json() if result.get('ok'): photo_id = result['result']['photo'][-1]['file_id'] 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}/" @@ -458,10 +567,41 @@ async def api_send_broadcast( success_count += 1 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)): """Настройки""" diff --git a/web/templates/broadcast.html b/web/templates/broadcast.html index 1ba1cd0..799e8cb 100644 --- a/web/templates/broadcast.html +++ b/web/templates/broadcast.html @@ -60,9 +60,20 @@ +
+ + + Выберите получателей рассылки +
+
- - Получатели: Все верифицированные пользователи + общий чат + + Получатели: Все верифицированные пользователи + общий чат
+ + + +
+
+
+
+
🔘 Кнопки действий:
+
    +
  • — Редактировать профиль
  • +
  • — Верифицировать
  • +
  • — Отозвать верификацию
  • +
  • — Забанить
  • +
  • — Разбанить
  • +
+
+
+
👥 Управление доступом:
+
    +
  • — Назначить админом
  • +
  • — Снять админа
  • +
  • — Добавить в ИГ
  • +
  • — Удалить из ИГ
  • +
+
+
+
+
@@ -130,6 +193,7 @@ {% for user in users %} + {% set is_in_ig = user.user_id in ig_user_ids %} @@ -177,6 +241,24 @@ {% endif %} + {% if user.is_admin %} + + {% else %} + + {% endif %} + {% if is_in_ig %} + + {% else %} + + {% endif %} {% 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; diff --git a/ИНСТРУКЦИЯ_ДЛЯ_ЖИЛЬЦОВ.md b/ИНСТРУКЦИЯ_ДЛЯ_ЖИЛЬЦОВ.md new file mode 100644 index 0000000..e78f6a7 --- /dev/null +++ b/ИНСТРУКЦИЯ_ДЛЯ_ЖИЛЬЦОВ.md @@ -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 +✅ **Расписание отключений** — будь в курсе +✅ **Рейтинг активности** — стань лидером дома! +✅ **Достижения** — получай за активность +✅ **Детект шпионов** — защита от ботов +✅ **Анти-мат и анти-спам** — чистый чат + +--- + +## ❓ ВОПРОСЫ + +**Не получается верифицироваться?** +→ Напиши админу в ЛС + +**Бот не отвечает?** +→ Проверь, что ты верифицирован + +**Нашёл баг?** +→ Сообщи админу + +--- + +**С уважением, администрация дома** 🏠
{{ user.user_id }} {{ user.full_name or '—' }}