# OREOL VPN Bot

Telegram-бот на `aiogram 3` с интеграцией в `Remnawave API`, локальным кэшем в `MariaDB`, пользовательской панелью, тикетами поддержки, реферальной системой и ручной оплатой с подтверждением админом.

Этот README написан как рабочий handoff-документ: по нему можно поднять проект, понять текущую архитектуру и продолжить разработку в новой сессии без восстановления контекста по кускам.

## Что умеет бот

- Показывает стартовую панель пользователя через `/start`
- Подтягивает аккаунты Remnawave по `telegramId`
- Хранит локальный кэш пользователей и истории подписок в MariaDB
- Поддерживает привязку существующего аккаунта Remnawave по `short_uuid`
- Показывает профиль, статус подписки, срок и трафик
- Работает с реферальными кодами и скидкой перед оплатой
- Поддерживает ручную оплату переводом с отправкой чека в бот
- Отправляет чек в отдельный Telegram-канал/топик на проверку
- Даёт админу или модератору подтвердить или отклонить оплату
- После подтверждения создаёт или продлевает доступ в Remnawave
- Поддерживает тикеты в отдельный канал и ответы только от админа/модератора
- Показывает админам отдельную Telegram-панель с метриками и быстрыми действиями

## Текущий сценарий оплаты

Сейчас в проекте не используется Telegram Stars. Оплата работает вручную:

1. Пользователь нажимает `Купить подписку`
2. Бот показывает список тарифов из `PAYMENT_PLANS`
3. Если у пользователя применён реферальный код, скидка учитывается до выбора тарифа
4. После выбора тарифа бот показывает реквизиты из `PAYMENT_TRANSFER_TEXT`
5. Пользователь отправляет чек следующим сообщением в бота
6. Бот пересылает чек в review-чат оплаты
7. Админ или модератор нажимает `Подтвердить` или `Отклонить`
8. При подтверждении бот создаёт или продлевает доступ в Remnawave и отправляет пользователю подписку

### Тарифы

Тарифы задаются одной переменной:

```env
PAYMENT_PLANS=30:250,180:600,365:1000
```

Формат:

- `дни:цена_в_рублях`
- элементы разделяются запятой

Пример выше означает:

- 30 дней = 250 ₽
- 180 дней = 600 ₽
- 365 дней = 1000 ₽

### Логин в Remnawave

Логин создаётся в формате:

```text
Oreol-<telegram_id>-<username>
```

Пример:

```text
Oreol-591220249-slkes
```

Если username отсутствует, используется `first_name`, а если и его нет, то `user`.

Важно:

- логин обрезается до 36 символов, чтобы соответствовать ограничениям Remnawave
- повторная покупка у того же пользователя не должна создавать новый логин, а должна продлевать уже существующий доступ

## Важное замечание по базе

Если таблица `payment_orders` уже была создана старой версией проекта, в ней мог остаться `UNIQUE` на `provision_username`.

Для текущей логики это неверно, потому что один и тот же логин может использоваться в нескольких заказах одного пользователя при продлении.

Правильное состояние:

- в ORM `provision_username` обычный индекс без `UNIQUE`
- в `sql/schema.sql` тоже без `UNIQUE`

Если таблица уже существует в MariaDB, может потребоваться вручную убрать старое уникальное ограничение.

## Архитектура

### Основные слои

- `app/main.py`
  - инициализация приложения
  - подключение к БД
  - создание клиента Remnawave
  - создание `SyncService`, `SupportTicketService`, `PaymentService`
  - запуск aiogram

- `app/config.py`
  - все настройки из `.env`
  - парсинг ID админов и модераторов
  - парсинг ссылок Telegram вида `https://t.me/c/.../.../...`
  - парсинг тарифов оплаты

- `app/services/remnawave_client.py`
  - прямой HTTP-клиент к Remnawave API через `httpx`
  - `GET /users/by-telegram-id/{telegramId}`
  - `GET /users/{uuid}`
  - `GET /users/by-short-uuid/{shortUuid}`
  - `GET /users/by-username/{username}`
  - `POST /users`
  - `PATCH /users`
  - `POST /users/resolve`
  - `GET /users`
  - `GET /users/{uuid}/subscription-request-history`

- `app/services/sync_service.py`
  - регистрация Telegram-пользователей
  - синхронизация пользователей из Remnawave в MariaDB
  - привязка аккаунтов по `short_uuid`
  - реферальные коды и приглашения

- `app/services/payment_service.py`
  - список доступных тарифов
  - создание ручного заказа
  - расчёт скидки по реферальному коду
  - хранение статусов заказа
  - создание нового доступа в Remnawave
  - продление существующего доступа в Remnawave

- `app/services/support_ticket_service.py`
  - создание тикетов
  - поиск тикета по support message ID
  - отметка, что тикет обработан

- `app/bot/handlers/main.py`
  - все команды и callback-и
  - стартовая панель
  - меню оплаты
  - FSM для тикетов, рефкодов и чека оплаты
  - приём ответа модератора на тикет
  - приём кнопок `Подтвердить / Отклонить` в review-канале оплаты

- `app/bot/ui/panel.py`
  - рендер панели пользователя
  - инлайн-кнопки разделов
  - тексты профиля, подписки, рефералки, правил и поддержки

- `app/utils/formatters.py`
  - форматирование дат
  - форматирование трафика
  - форматирование карточек доступа

## Структура проекта

```text
telegabot/
├── app/
│   ├── bot/
│   │   ├── handlers/
│   │   │   └── main.py
│   │   └── ui/
│   │       └── panel.py
│   ├── db/
│   │   ├── base.py
│   │   ├── models.py
│   │   └── session.py
│   ├── schemas/
│   │   └── remnawave.py
│   ├── services/
│   │   ├── payment_service.py
│   │   ├── remnawave_client.py
│   │   ├── support_ticket_service.py
│   │   └── sync_service.py
│   ├── utils/
│   │   └── formatters.py
│   ├── config.py
│   └── main.py
├── assets/
│   └── main.png
├── sql/
│   └── schema.sql
├── tests/
│   ├── test_config.py
│   ├── test_formatters.py
│   ├── test_panel_ui.py
│   └── test_remnawave_client.py
├── .env.example
├── docker-compose.yml
├── Dockerfile
├── main.py
├── pyproject.toml
├── README.md
└── run.bat
```

## Команды бота

### Пользовательские

- `/start` — открыть стартовую панель

### Админские

- `Админ-панель` в UI — сводка по боту, поиск пользователя, полная синхронизация, быстрые переходы в рабочие чаты
- `/lookup <uuid|id|username|short_uuid>` — найти и синхронизировать пользователя
- `/sync_all` — массовая синхронизация пользователей Remnawave в MariaDB

## База данных

### Основные таблицы

- `telegram_users`
  - Telegram-пользователи, взаимодействовавшие с ботом

- `remnawave_users`
  - локальный кэш пользователей Remnawave

- `internal_squads`
  - справочник внутренних групп

- `remnawave_user_internal_squads`
  - связь many-to-many пользователей и групп

- `subscription_request_logs`
  - история запросов подписки

- `support_tickets`
  - тикеты поддержки

- `referral_codes`
  - персональные коды пользователей

- `referral_invites`
  - кто кого пригласил

- `referral_bonuses`
  - начисленные и ожидающие применения бонусы реферерам

- `payment_orders`
  - ручные платежи, статусы и выданные доступы

### Статусы заказов оплаты

Сейчас используются:

- `PENDING` — заказ создан, чек ещё не отправлен
- `REVIEW` — чек отправлен на проверку
- `REJECTED` — платёж отклонён модератором
- `FULFILLED` — доступ выдан или продлён

## Реферальная система

Что реализовано:

- каждому пользователю создаётся персональный код
- можно сгенерировать ссылку вида `https://t.me/<bot_username>?start=ref_<CODE>`
- сам переход по ссылке скидку не активирует
- чтобы получить скидку, пользователь должен вручную ввести код до оплаты
- размер скидки задаётся в `REFERRAL_DISCOUNT_PERCENT`
- за каждую подтверждённую оплату по рефкоду реферер получает бонус в днях
- размер бонуса задаётся в `REFERRAL_BONUS_DAYS`

## Тикеты поддержки

Сценарий:

1. Пользователь открывает раздел `Поддержка`
2. Нажимает `Создать тикет`
3. Отправляет следующим сообщением вопрос или описание проблемы
4. Бот отправляет тикет в support-чат
5. Админ или модератор отвечает реплаем на сообщение с тикетом
6. Бот доставляет ответ пользователю в личный чат

Доступ к ответу на тикет есть только у:

- пользователей из `BOT_ADMIN_IDS`
- пользователей из `BOT_MODERATOR_IDS`

## Переменные окружения

Полный шаблон лежит в `.env.example`.

Критический минимум для запуска:

- `BOT_TOKEN`
- `REMNAWAVE_BASE_URL`
- `REMNAWAVE_API_TOKEN`
- `DB_HOST`
- `DB_PORT`
- `DB_NAME`
- `DB_USER`
- `DB_PASSWORD`
- `PAYMENT_INTERNAL_SQUAD_UUIDS`

Для полного сценария поддержки и оплаты также нужны:

- `BOT_PUBLIC_USERNAME`
- `BOT_SUPPORT_TICKET_LINK` или `BOT_SUPPORT_TICKET_CHAT_ID`
- `PAYMENT_TRANSFER_TEXT`
- `PAYMENT_REVIEW_LINK` или `PAYMENT_REVIEW_CHAT_ID`

## Как настроить `.env`

1. Скопировать шаблон:

```bat
copy .env.example .env
```

2. Заполнить обязательные значения

3. Проверить:

- корректен ли `BOT_TOKEN`
- доступен ли `REMNAWAVE_BASE_URL`
- валиден ли `REMNAWAVE_API_TOKEN`
- существует ли MariaDB и есть ли права на создание БД, если `CREATE_DATABASE_ON_START=true`
- указаны ли реальные UUID групп в `PAYMENT_INTERNAL_SQUAD_UUIDS`
- указаны ли реальные реквизиты в `PAYMENT_TRANSFER_TEXT`

## Пример важных настроек

```env
BOT_BRAND_NAME=OREOL VPN
BOT_PUBLIC_USERNAME=oreol_vpn_bot

DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=oreolvpn
DB_USER=oreolvpn
DB_PASSWORD=strong_password

PAYMENT_PLANS=30:250,180:600,365:1000
PAYMENT_TRANSFER_TEXT=Карта 0000 0000 0000 0000; банк OREOL; получатель OREOL VPN
PAYMENT_REVIEW_LINK=https://t.me/c/3646494169/56/57
PAYMENT_INTERNAL_SQUAD_UUIDS=11111111-1111-1111-1111-111111111111
REFERRAL_DISCOUNT_PERCENT=5
PAYMENT_USERNAME_PREFIX=Oreol
```

## Поддержка Telegram-ссылок `t.me/c/...`

Проект умеет разбирать приватные ссылки Telegram вида:

```text
https://t.me/c/3646494169/56/57
```

Разбор происходит так:

- chat id = `-1003646494169`
- thread id = `56`
- последний сегмент `57` — это message id, для маршрутизации топика он не нужен

Это используется для:

- `BOT_SUPPORT_TICKET_LINK`
- `PAYMENT_REVIEW_LINK`

## Как запускать

### Вариант 1. Windows через `run.bat`

```bat
run.bat
```

Что делает батник:

- переходит в папку проекта
- проверяет `.env`
- создаёт `.venv`, если его нет
- ставит зависимости
- запускает `main.py`

### Вариант 2. Ручной локальный запуск

```bat
py -3 -m venv .venv
.venv\Scripts\python -m pip install -e .[dev]
.venv\Scripts\python main.py
```

### Вариант 3. Docker

```bash
docker compose up --build
```

Это полный запуск вместе с MariaDB. База данных уже есть в проекте: сервис `db` описан в `docker-compose.yml`, данные сохраняются в volume `mariadb_data`.

#### Что нужно установить заранее

- Docker Desktop на Windows/macOS
- или Docker Engine + Docker Compose plugin на Linux

Проверьте, что команды доступны:

```bash
docker --version
docker compose version
```

#### Ubuntu Server 24.04: установка Docker Engine

Для `Ubuntu Server 24.04` рекомендуется ставить Docker Engine из официального Docker-репозитория.

1. Обновить пакеты и поставить базовые утилиты:

```bash
sudo apt update
sudo apt install -y ca-certificates curl git
```

2. Добавить официальный GPG-ключ Docker:

```bash
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
```

3. Подключить Docker repository:

```bash
sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
```

4. Установить Docker Engine и Compose plugin:

```bash
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
```

5. Включить Docker после перезагрузки:

```bash
sudo systemctl enable docker --now
```

6. Разрешить запуск Docker без `sudo`:

```bash
sudo usermod -aG docker $USER
```

После этого выйдите из SSH-сессии и зайдите снова, либо выполните:

```bash
newgrp docker
```

7. Проверить установку:

```bash
docker --version
docker compose version
docker ps
```

Если проект будет стоять на VPS или выделенном сервере, этого достаточно: отдельный Docker Desktop на Linux Server не нужен.

#### Что нужно подготовить перед первым запуском

1. Скопировать пример конфига:

```bash
cp .env.example .env
```

На Windows PowerShell:

```powershell
Copy-Item .env.example .env
```

2. Открыть `.env` и заполнить обязательные значения:

- `BOT_TOKEN`
- `BOT_ADMIN_IDS`
- `REMNAWAVE_BASE_URL`
- `REMNAWAVE_API_TOKEN`
- `DB_NAME`
- `DB_USER`
- `DB_PASSWORD`
- `DB_ROOT_PASSWORD`
- `PAYMENT_INTERNAL_SQUAD_UUIDS`

Обычно ещё сразу настраивают:

- `BOT_PUBLIC_USERNAME`
- `BOT_SUPPORT_URL`
- `BOT_SUPPORT_TICKET_LINK` или `BOT_SUPPORT_TICKET_CHAT_ID`
- `PAYMENT_TRANSFER_TEXT`
- `PAYMENT_REVIEW_LINK` или `PAYMENT_REVIEW_CHAT_ID`

#### Важный момент по базе данных в Docker

При запуске через `docker compose` контейнер бота сам получает:

```env
DB_HOST=db
DB_PORT=3306
```

Это уже задано в `docker-compose.yml`. В `.env` для Docker главное задать:

- `DB_NAME`
- `DB_USER`
- `DB_PASSWORD`
- `DB_ROOT_PASSWORD`

Если вы запускаете проект только в Docker, `DB_HOST` и `DB_PORT` можно не менять вручную.

`DB_ROOT_PASSWORD` нужен только контейнеру MariaDB. Сам бот подключается к базе исключительно через:

- `DB_USER`
- `DB_PASSWORD`

Это важно:

- `DB_ROOT_PASSWORD` не используется приложением для SQLAlchemy-подключения
- если в `.env` указать `DB_USER=root`, но оставить `DB_PASSWORD` пустым, бот попытается зайти как `root` без пароля и упадёт с `Access denied`

Для production рекомендуется не использовать `root` для бота, а создать отдельного пользователя БД через:

- `DB_USER`
- `DB_PASSWORD`

MariaDB в текущем `docker-compose.yml` не публикуется наружу на хост, чтобы база не торчала во внешний интернет по `3306`. Бот подключается к ней по внутренней Docker-сети через hostname `db`.

#### Первый запуск

Запустите сборку и контейнеры:

```bash
docker compose up -d --build
```

Будут подняты два сервиса:

- `db` — MariaDB 11.7
- `bot` — приложение на Python

Полный сценарий для Ubuntu Server обычно выглядит так:

```bash
git clone <URL_репозитория> telegabot
cd telegabot
cp .env.example .env
nano .env
docker compose up -d --build
```

#### Как проверить, что всё поднялось

Посмотреть список контейнеров:

```bash
docker compose ps
```

Посмотреть логи бота:

```bash
docker compose logs -f bot
```

Посмотреть логи базы:

```bash
docker compose logs -f db
```

Проверить, что бот видит базу и база отвечает:

```bash
docker compose exec db mariadb -u"$DB_USER" -p"$DB_PASSWORD" -e "SHOW DATABASES;"
```

Если нужно зайти в MariaDB вручную:

```bash
docker compose exec db mariadb -u"$DB_USER" -p"$DB_PASSWORD" "$DB_NAME"
```

Если всё настроено правильно, сценарий такой:

1. поднимается `db`
2. healthcheck MariaDB становится `healthy`
3. стартует `bot`
4. бот при старте создаёт БД и таблицы, если включены:
   - `CREATE_DATABASE_ON_START=true`
   - `CREATE_TABLES_ON_START=true`

#### Остановка и повторный запуск

Остановить контейнеры:

```bash
docker compose down
```

Запустить снова без пересборки:

```bash
docker compose up -d
```

Пересобрать после изменения кода:

```bash
docker compose up -d --build
```

#### Как удалить всё вместе с базой

Если нужен полный сброс, включая MariaDB volume:

```bash
docker compose down -v
```

После этого база будет создана заново при следующем запуске.

#### Обновление на Ubuntu Server

Если вы обновили код проекта:

```bash
git pull
docker compose up -d --build
```

Если меняли только `.env`, обычно достаточно:

```bash
docker compose up -d
```

#### Если бот не стартует

Проверьте по пунктам:

- заполнен ли `BOT_TOKEN`
- доступен ли `REMNAWAVE_BASE_URL`
- корректен ли `REMNAWAVE_API_TOKEN`
- заполнен ли `PAYMENT_INTERNAL_SQUAD_UUIDS`
- не пустые ли `DB_PASSWORD` и `DB_ROOT_PASSWORD`
- есть ли у сервиса `db` статус `healthy` в `docker compose ps`

Если видите ошибку вида:

```text
Access denied for user 'root' ... (using password: NO)
```

проверьте:

- не стоит ли в `.env` `DB_USER=root`
- не пустой ли `DB_PASSWORD`
- совпадают ли `DB_USER` и `DB_PASSWORD` с пользователем MariaDB, который был создан при первом старте контейнера

Если контейнер `db` уже один раз поднимался с неправильными DB-переменными, volume MariaDB мог сохранить старого пользователя и старый пароль. Тогда есть два варианта:

- если данных ещё не жалко: `docker compose down -v` и затем `docker compose up -d --build`
- если данные нужно сохранить: зайти в MariaDB и вручную создать/выдать права нужному пользователю

Если хотите открыть MariaDB наружу для внешнего клиента, это нужно делать осознанно: добавьте `ports` обратно в сервис `db` и отдельно ограничьте доступ через firewall или private network.

#### Минимальный сценарий запуска в Docker

1. Скопировать `.env.example` в `.env`
2. Заполнить Telegram, Remnawave и DB-переменные
3. Выполнить `docker compose up -d --build`
4. Проверить `docker compose logs -f bot`

## Проверка проекта

### Синтаксис

```bat
py -3 -m compileall app main.py tests
```

### Тесты

```bat
.venv\Scripts\python -m pytest
```

Если основная `.venv` невалидна из-за переезда проекта между машинами, пересоздай её.

## Что уже протестировано

Покрыто тестами:

- парсинг конфигурации
- разбор support/review ссылок Telegram
- парсинг тарифов
- форматтеры текста
- UI панели
- базовые методы `RemnawaveApiClient`

Последняя локальная проверка в этой сетевой копии:

- `py -3 -m compileall app main.py tests` — успешно
- `pytest` — `26 passed`

## Известные нюансы

### 1. Перенос `.venv` между машинами

Если скопировать проект вместе с уже готовой `.venv` в другую папку, на другой компьютер или на сетевой диск, окружение может ссылаться на старый путь Python.

Типичная ошибка:

```text
did not find executable at ...python.exe
```

Решение:

1. удалить `.venv`
2. создать её заново
3. снова установить зависимости

### 2. Ошибка `Unknown database`

Если MariaDB доступна, но самой базы из `DB_NAME` ещё нет, можно увидеть:

```text
OperationalError: (1049, "Unknown database '...'" )
```

Что делать:

- оставить `CREATE_DATABASE_ON_START=true`, если у пользователя есть права на создание БД
- либо создать БД вручную

### 3. Прокси Telegram

Для `TELEGRAM_PROXY_URL` подходят обычные proxy URL:

- `http://user:pass@host:port`
- `socks5://host:port`

Не подходят клиентские ссылки вроде:

- `vless://...`
- `vmess://...`
- `trojan://...`
- `tg://proxy?...` для aiogram напрямую

### 4. Review-чат оплаты должен быть настроен

Если `PAYMENT_REVIEW_LINK` и `PAYMENT_REVIEW_CHAT_ID` пустые, пользователь сможет выбрать тариф, но бот не сможет отправить чек на модерацию.

### 5. Internal squads обязательны для выдачи доступа

Если `PAYMENT_INTERNAL_SQUAD_UUIDS` пустой, бот не сможет корректно создать доступ в Remnawave.

## Безопасность

Нельзя публиковать:

- реальный `.env`
- `BOT_TOKEN`
- `REMNAWAVE_API_TOKEN`
- `REMNAWAVE_CADDY_API_KEY`
- банковские реквизиты из рабочего `PAYMENT_TRANSFER_TEXT`

Передавать между сессиями безопасно:

- код проекта
- `README.md`
- `.env.example`

## Что логично делать дальше

Следующие разумные шаги по проекту:

- добавить миграции через Alembic
- хранить отдельный audit log по модерации оплат
- сделать уведомление модераторам о новых чеках
- добавить историю заказов пользователя в панели
- сделать админ-панель со списком платежей
- добавить продление конкретного существующего аккаунта по выбору, если у пользователя их несколько
- добавить отдельный статус `APPROVED_BUT_NOT_DELIVERED`, если бот не смог отправить результат пользователю
- покрыть тестами `PaymentService` и FSM оплаты

## Быстрый handoff

Если проект открывает новая сессия, порядок такой:

1. Прочитать этот README
2. Проверить `.env`
3. Проверить, рабочая ли `.venv`
4. Убедиться, что доступны MariaDB и Remnawave
5. Запустить `run.bat` или `python main.py`
6. Проверить руками:
   - `/start`
   - открытие раздела подписки через кнопку в панели
   - ввод реферального кода
   - отправку чека
   - подтверждение оплаты модератором
   - выдачу ссылки пользователю
   - поддержку через тикет
