No description
  • JavaScript 50.5%
  • Python 37.2%
  • CSS 10.8%
  • Shell 1.1%
  • Dockerfile 0.2%
  • Other 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
pxc1984 556ea90fa0
All checks were successful
CI/CD / Build and push backend image (push) Successful in 16s
CI/CD / Build and push bot image (push) Successful in 12s
CI/CD / Build and push frontend image (push) Successful in 10s
CI/CD / Deploy to dev-1 (push) Successful in 7s
CI/CD / Deploy to dev (push) Successful in 0s
CI/CD / Deploy to prod-1 (push) Has been skipped
CI/CD / Deploy to prod (push) Successful in 0s
ci: move CI/CD to Forgejo Actions, images to git.cu3rd.ru registry
2026-10-27 15:13:23 +00:00
.forgejo/workflows ci: move CI/CD to Forgejo Actions, images to git.cu3rd.ru registry 2026-10-27 15:13:23 +00:00
.idea backend refactor 2026-09-01 13:02:12 +03:00
backend verify_webapp_init_data fix 2026-09-03 15:35:38 +03:00
bot Добавлен поиск 'идеального соседа', оповещения в группу тг и фикс отображения имён 2026-07-22 15:45:17 +03:00
frontend style: align tracker script with document head 2026-10-27 16:21:34 +03:00
scripts ci set up 2026-09-01 11:39:04 +03:00
traefik тихий дроп запросов к внутренним ручкам /api/bot 2026-09-01 12:25:18 +03:00
.env.example ci set up 2026-09-01 11:39:04 +03:00
.gitignore ci set up 2026-09-01 11:39:04 +03:00
DEPLOY.md ci: move CI/CD to Forgejo Actions, images to git.cu3rd.ru registry 2026-10-27 15:13:23 +00:00
docker-compose.prod.yml don't need those stinky profile: on .prod 2026-09-01 11:54:48 +03:00
docker-compose.yml Добавлен поиск 'идеального соседа', оповещения в группу тг и фикс отображения имён 2026-07-22 15:45:17 +03:00
LICENSE Add Apache License 2.0 2026-08-12 17:18:11 +03:00
README.md Добавлены блоки комнат 2026-07-21 11:33:28 +03:00

🏠 Кампус-отели Диск и Облако — поиск соседей по комнате

Сервис для поиска соседей в комнату кампус-отеля (на 2–4 человека). Показывает анкеты студентов с фото и краткой информацией, а связаться можно прямо в Telegram.

Стек

  • Бэкенд: Python, FastAPI, SQLAlchemy
  • База данных: PostgreSQL
  • Фронтенд: React, Vite (в проде — сборка отдаётся через nginx)
  • Инфраструктура: Docker, Docker Compose

Возможности

  • Выбор пола на входе: парни живут с парнями, девушки — с девушками, поэтому первым экраном спрашивается пол, и лента показывает только «своих». Выбор сохраняется в localStorage и меняется кнопкой «Сменить» в шапке
  • Фото: загружается файлом (не ссылкой). Бэкенд проверяет, что это картинка, ужимает до 800px по большей стороне и сохраняет как JPEG
  • Аватар из Telegram: можно подставить фото и ник из профиля Telegram — через Login Widget (обычный веб) или Mini App SDK (сайт открыт внутри Telegram). Требует токен бота, см. раздел ниже
  • Три раздела: «Ищут соседей» (кто ещё сам по себе), «Комнаты» (кто уже договорился) и «Блок». Комната — просто группа людей, без номеров и секций: видно состав, сколько мест занято (2/3) и сколько свободно. Двое сошлись — собирают комнату на 3 и ищут третьего; человек из комнаты пропадает из раздела одиночек
  • Блоки: в «Диске» комнаты объединяются попарно в блок на 6 человек — 2+4 или 3+3. Объединяются целыми комнатами: одна зовёт, и согласиться должны все жильцы позванной. Комната в блоке не меняет размер (иначе в блоке перестанет быть 6), а выход одной комнаты распускает блок целиком. В «Облаке» блоков нет, и вкладка там не показывается
  • Вступление только по согласию: никто не заходит в комнату сам. Летит заявка, и её должны подтвердить все, кто уже внутри. Любой отказ — заявка отклонена
  • Бот в Telegram пишет про новые заявки (с кнопками «Принять / Отклонить»), про решение по твоей заявке и про выход соседа из комнаты
  • Лента анкет карточками: фото, имя, направление, о себе. Направление — одно из пяти: Разработка, Бизнес, Дизайн, ИИ, Не определился. Возраст и курс не собираются и не показываются (колонки age/course оставлены в БД nullable — если понадобятся, вернуть легко)
  • Метки: режим сна (сова/жаворонок), курение, чистоплотность, желаемый размер комнаты. Размеров можно указать несколько — «хочу 3 или 4, но не двухместную»; ничего не выбрано — подойдёт любая
  • Фильтры: размер комнаты, режим сна, курение + поиск по тексту
  • Кнопка «Написать в Telegram» ведёт на t.me/username
  • Форма «Разместить анкету» — добавление своего профиля (пол подставляется автоматически)
  • Темы: светлая и тёмная, по умолчанию — системная. Переключатель в шапке циклом: Системная → Светлая → Тёмная. Выбор хранится в localStorage, в режиме «Системная» приложение следит за сменой темы ОС на лету

Деплой на сервер

Разворачивание на VPS с доменом и HTTPS — отдельная инструкция: DEPLOY.md. Там прод-конфиг docker-compose.prod.yml: наружу торчит только Caddy с автоматическим сертификатом, база и бэкенд закрыты.

Запуск через Docker (рекомендуется)

Всё поднимается одной командой — Postgres, бэкенд и фронтенд:

docker compose -p obshaga up -d --build

Флаг -p obshaga задаёт имя проекта, потому что имя папки кириллическое и Docker его не принимает как имя проекта по умолчанию.

После старта:

При первом старте бэкенд дожидается готовности БД и создаёт таблицы. Демо-анкет нет — сервис стартует пустым, первые анкеты появляются, когда их разместят люди. Данные хранятся в volume pgdata и переживают перезапуск контейнеров.

Полезные команды:

docker compose -p obshaga logs -f backend   # логи бэкенда
docker compose -p obshaga down              # остановить (данные сохранятся)
docker compose -p obshaga down -v           # остановить и стереть данные БД

Telegram: бот и аватарки

Куда класть токен

Скопируй пример и заполни — docker compose подхватит .env сам, файл уже в .gitignore:

cp .env.example .env
TELEGRAM_BOT_TOKEN=123456789:AA...      # от @BotFather
TELEGRAM_BOT_USERNAME=my_campus_bot     # ник бота без @
BOT_SECRET=<длинная случайная строка>   # openssl rand -hex 32
SITE_URL=https://твой-домен            # для ссылок в сообщениях бота

Запуск вместе с ботом (бот под профилем, чтобы без токена не мешал):

docker compose -p obshaga --profile bot up -d --build
docker compose -p obshaga logs -f bot

BOT_SECRET — общий секрет бота и бэкенда. Бот действует от имени Telegram-пользователя, поэтому служебные ручки /api/bot/* закрыты этим заголовком: без него голосовать за других смог бы кто угодно.

Как включить уведомления (со стороны студента)

  1. разместить анкету на сайте, указав свой ник;
  2. написать боту /start — он найдёт анкету по нику (или по подтверждённому Telegram-аккаунту) и запомнит чат.

Шаг 2 обязателен: бот не может написать первым тому, кто не начал с ним диалог — это ограничение Telegram, а не проекта. Пока человек не нажал /start, уведомления ему просто не уходят, сайт при этом работает как обычно.

Команды бота: /start — включить уведомления, /pending — заявки, ждущие твоего решения.

Аватарки из профиля

Без этих настроек кнопка входа через Telegram просто не показывается, а загрузка фото файлом работает как обычно.

Важно понимать ограничение: получить аватар по @username нельзя. Bot API отдаёт фото только тем, кто уже писал боту, и ищет по user_id, а не по нику. Поэтому аватар берётся только после того, как человек сам подтвердит вход.

1. Завести бота

В @BotFather: /newbot → получаешь токен вида 123456789:AA.... Затем /setdomain → указываешь домен сайта.

2. Передать токен

export TELEGRAM_BOT_TOKEN="123456789:AA..."
export TELEGRAM_BOT_USERNAME="my_campus_bot"
docker compose -p obshaga up -d

3. Что заработает

  • Login Widget — на обычном сайте появится кнопка «Log in with Telegram». Работает только на домене из /setdomain: на localhost Telegram кнопку не покажет, отсюда нужен боевой домен с HTTPS.
  • Mini App — если открыть сайт внутри Telegram (кнопка у бота через /setmenubutton или inline-кнопка web_app), приложение само предложит «Взять фото и ник из Telegram». Нужен публичный HTTPS-адрес.

Как это защищено

Telegram подписывает данные HMAC-SHA256 на основе токена бота, и сервер проверяет подпись сам — схемы у Login Widget и Mini App разные и не взаимозаменяемы. Подпись старше суток отклоняется.

Галочку «подтверждён» ставит только сервер после повторной проверки подписи при создании анкеты: клиент не может объявить себя подтверждённым, а ник берётся из подтверждённых данных, чтобы нельзя было указать чужой.

Аватар скачивается и сохраняется у нас — ссылки t.me/i/userpic/... живут недолго.

Запуск без Docker (для разработки)

Нужен запущенный PostgreSQL. Строка подключения задаётся переменной DATABASE_URL (по умолчанию postgresql+psycopg2://obshaga:obshaga@localhost:5432/obshaga).

1. Бэкенд (порт 8000)

cd backend
python3 -m venv venv
source venv/bin/activate                    # Windows: venv\Scripts\activate
pip install -r requirements.txt
export DATABASE_URL=postgresql+psycopg2://obshaga:obshaga@localhost:5432/obshaga
uvicorn main:app --reload --port 8000

2. Фронтенд (порт 5173)

cd frontend
npm install
npm run dev

Открыть http://localhost:5173 . В dev-режиме запросы к /api проксируются на бэкенд (настроено в vite.config.js).

API

Метод Путь Описание
GET /api/profiles Список анкет (+ фильтры в query)
GET /api/profiles/{id} Одна анкета
POST /api/profiles Создать анкету
GET /api/groups Комнаты (+ gender, only_open)
GET /api/groups/{id} Одна комната с составом
POST /api/groups Собрать комнату
POST /api/groups/{id}/leave Выйти
GET /api/groups/{id}/requests Заявки в эту комнату
POST /api/groups/{id}/requests Подать заявку
POST /api/requests/{id}/vote Голос жильца (за/против)
POST /api/requests/{id}/cancel Отозвать свою заявку
GET /api/profiles/{id}/requests Мои заявки
GET /api/blocks Собранные блоки (+ gender, campus)
GET /api/groups/{id}/block-candidates Комнаты, с которыми выйдет полный блок
GET /api/groups/{id}/block-requests Заявки на блок этой комнаты
POST /api/blocks/requests Позвать комнату в блок
POST /api/blocks/requests/{id}/vote Голос жильца позванной комнаты
POST /api/blocks/requests/{id}/cancel Отозвать предложение
POST /api/blocks/{id}/leave Выйти из блока своей комнатой
POST /api/uploads/photo Загрузить фото (multipart)
GET /api/media/{file} Отдача загруженных фото
POST /api/auth/telegram Вход через Login Widget
POST /api/auth/telegram/webapp Вход из Mini App (initData)
GET /api/config Включён ли Telegram, лимит фото
GET /api/health Проверка статуса

Фильтры для GET /api/profiles: gender, track, room_capacity, smoking, sleep_schedule, search, without_group (true — только те, кто ещё не в комнате). room_capacity — одно число: подходят те, кто выбрал этот размер среди желаемых, и те, кому размер не важен.

track — одно из dev | business | design | ai | undecided; русские подписи живут на фронтенде (src/labels.js). search ищет по имени и «о себе»: направление теперь выбирают фильтром, а не текстом.

Правила комнат и заявок

Проверяются на сервере:

  • вступить напрямую нельзя — только заявка;
  • заявку должны подтвердить все, кто сейчас в комнате; любой отказ — отклонение;
  • голосовать могут только жильцы этой комнаты (403), сам заявитель — не может;
  • в комнату пускают только свой пол (403);
  • нельзя состоять в двух комнатах сразу (409), нельзя подать вторую заявку в ту же комнату (409);
  • места перепроверяются в момент одобрения: пока голосовали, их могли занять;
  • голос вышедшего перестаёт учитываться — иначе заявка висела бы вечно;
  • когда выходит последний жилец, комната удаляется.

Правила блоков

  • блоки есть только в «Диске» (campuses.WITH_BLOCKS), размер блока — 6 человек;
  • пара подбирается по размеру комнат: 2+4 или 3+3, третью комнату не добавить;
  • предложение должны принять все жильцы позванной комнаты; любой отказ — отклонение (то же правило, что у заявок в комнату);
  • пол и кампус-отель у комнат должны совпадать;
  • комната в блоке не меняет размер (409) — иначе в блоке перестанет быть 6;
  • пара перепроверяется в момент одобрения: пока голосовали, размер комнаты могли изменить или она успела собрать блок с другими;
  • выход одной комнаты распускает блок целиком — половина блока смысла не имеет;
  • когда комната распадается, её блок распускается вместе с ней.

Авторизации в проекте нет, поэтому «моя анкета» — это её id в localStorage, он запоминается после создания анкеты. Этого достаточно, чтобы вступать и выходить, но постороннему ничто не мешает отправить чужой profile_id напрямую в API — для настоящего продакшена сюда нужна авторизация.

Структура

общага/
├── docker-compose.yml   # Postgres + бэкенд + фронтенд
├── backend/
│   ├── main.py          # FastAPI-приложение и эндпоинты
│   ├── models.py        # SQLAlchemy-модель Profile
│   ├── schemas.py       # Pydantic-схемы
│   ├── database.py      # подключение к Postgres + ожидание готовности БД
│   ├── requirements.txt
│   └── Dockerfile
└── frontend/
    ├── src/
    │   ├── App.jsx
    │   ├── api.js
    │   ├── labels.js
    │   ├── index.css
    │   └── components/
    │       ├── RoommateCard.jsx
    │       ├── Filters.jsx
    │       └── AddProfileModal.jsx
    ├── index.html
    ├── vite.config.js
    ├── nginx.conf       # прод-конфиг: статика + прокси /api
    └── Dockerfile       # сборка Vite → nginx