Skip to content

Latest commit

 

History

17 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

hackbot

Телеграм-бот, который ведёт таймлайн хакатона в отдельной теме группового чата: следит за дедлайнами, напоминает, собирает документы и складывает всё в репозиторий.

Один бот — много хакатонов. Каждый привязан к своей теме форума (chat_id + message_thread_id), поэтому «ЛЦТ 26» и «Вайбатон на Руби» живут рядом и не мешают друг другу.

Что он делает

Живая карточка. Одно закреплённое сообщение в теме, которое бот редактирует, а не пересылает: статус, прогресс-бар, обратный отсчёт до сдачи, что дальше, сколько человек подтвердило явку. Обновляется раз в минуту, чат не засоряет.

Таймлайн. Этапы с типами (регистрация, тех-чек, старт, чек-поинт, менторская, код-фриз, сдача, защита, результаты, афтепати), утренний дайджест, предупреждение о накладках.

Напоминания приходят за 3 дня, сутки, 3 часа, час и 15 минут до этапа (у дедлайна сдачи есть ещё и за 30 минут). Ступени, которые уже прошли на момент создания этапа, просто не срабатывают — добавил этап за два часа, получишь только две последние.

Заполнение с афиши и из файлов. /new с фотографией условий — vision-модель вытаскивает даты, площадку, ссылки и весь набор этапов. Приложенные PDF, DOCX и текстовые файлы бот читает и разбирает наравне с текстом сообщения. Сканированный PDF без текстового слоя честно попросит скриншот. Чего не нашёл — спросит, и на вопросы можно ответить обычным реплаем.

Свободный текст. Упомяни бота и скажи словами: «перенеси защиту на 19:30», «добавь код-фриз в пятницу 18:00», «сколько осталось до сдачи». Если данных не хватает — переспросит, а не выдумает.

Явка. Под напоминаниями кнопки ✅ / ⏰ / ❌ — без тегов и шума. Поимённый пинг только по явной команде /ping.

GitHub. /repo new создаёт <название>_<год> в организации и заливает docs/ с README, где таймлайн уже в виде таблицы. /repo attach прикрепляет существующий репозиторий, чтобы ссылка была под рукой в нужной теме.

Календарь. /ics отдаёт файл, а http://<host>:9999/ics/<slug>.ics — живую ленту: подписался один раз, правки таймлайна приезжают сами.

Google Календарь. Необязательная интеграция поверх этого: бот раскладывает этапы всех хакатонов в один ваш календарь. Календарь вы создаёте сами и даёте сервис-аккаунту бота право «Внесение изменений в мероприятия» — владелец вы, бот только пишет события. Календарь общий, поэтому хакатон видно прямо в заголовке: 🏁 Сдача проекта · ЛЦТ 26. Дальше он шарится команде обычным гугловым способом. Пока GOOGLE_CALENDAR_ID пуст, интеграция выключена целиком и бот ведёт себя ровно как раньше. Настройка по шагам — docs/google-calendar.md.

Мудрость дня. /wisdom (он же /совет, /имба) — абсурдный совет в жанре мотивационных карточек. В ответ на чьё-то сообщение — пингует человека и просит его версию. Работает и словами: «дай совет», «расскажи имбу».

Стикеры. С вероятностью 25% после своего сообщения бот кидает случайный стикер из пака. Настраивается через STICKER_SET и STICKER_CHANCE.

Помнит, с кем говорит. Профиль привязан к telegram id, а не к нику: ник можно сменить, id нельзя. Личность (id, ник, имя, счётчик сообщений) записывается сама на каждое сообщение, которое бот видит — иначе на вопрос «а Саня кто?» отвечать было бы нечем, Саня бота никогда не тегнет. Характер и род занятий пишутся только когда сказали явно: «запомни, я бэкендер на го». Факты дополняются, а не затираются. /whois — посмотреть, /forgetme — стереть.

Влезает в разговор. С вероятностью BANTER_CHANCE (по умолчанию 25%) бот отвечает на сообщение, в котором его не упоминали, глядя на последние BANTER_CONTEXT реплик темы. Между такими репликами держится пауза BANTER_COOLDOWN_SECONDS, чтобы всплеск сообщений не превратился в монолог. Работает во всех темах, включая General: разговор чаще идёт там, а не в теме хакатона. BANTER_EVERYWHERE=false вернёт его только в темы с хакатоном — это же переключатель и для реакций. Модель может вернуть ПРОПУСК и промолчать, если влезать не к месту.

Ставит реакции. С вероятностью REACTION_CHANCE (по умолчанию 25%) бот молча вешает на сообщение эмодзи — 🤡 на «задеплою в прод в пятницу», 🤓 на зануду, 🎉 на победу, 😭 на потерянный код. Список доступных эмодзи задан в agent/reaction.py: телеграм принимает только фиксированный набор, а чат может сузить его ещё сильнее. Ответ приходит обычным текстом и проверяется по списку в коде, а не через enum-схему: structured output у Ollama едет на tool calling, и модели на нём спотыкаются — на «го спать уже три часа ночи» enum сжигал обе попытки, а тот же вопрос обычным текстом с первого раза отвечал 🥱.

Модель может не выбрать ничего — на «созвон в 15:00» реакции не будет, и в этом смысл.

Реакции ставятся на любое сообщение, которое бот прочитал, — и на те, где его позвали, и на те, где не звали. Пауза REACTION_COOLDOWN_SECONDS общая на оба случая: реакция, появляющаяся на каждом втором сообщении, перестаёт что-либо значить, и неважно, каким путём она там оказалась.

Оба этих пути никогда не смотрят вложения. Документы и фото читаются только когда бота упомянули или ответили на его сообщение — сообщение с файлом пропускается целиком, даже если к нему есть подпись.

Характер бота

Промпты лежат в prompts/ обычным текстом и перечитываются на лету — поправил файл, следующее сообщение уже с новым характером, перезапуск не нужен.

файл что задаёт
prompts/persona.md как бот разговаривает и как огрызается
prompts/wisdom.md генератор шуточных советов
prompts/banter.md правила незваной реплики; идёт в дополнение к persona.md
prompts/reaction.md по какому принципу выбирается эмодзи-реакция

Блоки в <!-- --> вырезаются перед отправкой в модель — там можно держать заметки для себя. Если файл удалить, включится встроенный вариант из исходников.

Запасной ключ. Если задан OLLAMA_API_KEY_FALLBACK, каждый вызов модели оборачивается в FallbackModel: запрос, упавший с ошибкой провайдера, тут же повторяется со вторым ключом. Это покрывает выбранную квоту (429) и отозванный ключ (401) — то есть случаи, когда перестал работать ключ, а не сама модель. Запасной ключ подставляется только для Ollama: отправить его в другого провайдера значило бы превратить один упавший запрос в два.

Стек

Python 3.13 · aiogram 3 · pydantic-ai поверх Ollama Cloud · SQLAlchemy 2 async + SQLite · FastAPI. Без Docker, без внешней очереди, без отдельного планировщика: всё живёт в одном процессе и одном файле базы.

Запуск

cp .env.example .env      # заполнить BOT_TOKEN и ключи
uv sync
uv run python -m hackbot

Бот поднимается на long polling, веб-страница и .ics-ленты — на WEB_PORT (по умолчанию 9999).

Настройка бота в Telegram

  1. У @BotFather: Bot Settings → Group Privacy → Turn off — иначе бот не увидит упоминания в реплаях и вложения без команды.
  2. Добавить в группу администратором с правами «Закреплять сообщения» и «Управлять темами».
  3. Зайти в нужную тему и написать /new Название хакатона.

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

Переменная Смысл
BOT_TOKEN токен от @BotFather
BOT_ADMIN_IDS кому можно менять данные; пусто = админам чата
OLLAMA_API_KEY, OLLAMA_BASE_URL Ollama Cloud, OpenAI-совместимый /v1
OLLAMA_API_KEY_FALLBACK запасной ключ: подхватывается, когда основной отвечает ошибкой
LLM_MODEL ReAct-агент, нужен tool calling
LLM_VISION_MODEL разбор афиш, нужен приём картинок
LLM_FUN_MODEL генератор шуток; вынесен отдельно ради качества русского
GITHUB_TOKEN, GITHUB_ORG, GITHUB_PRIVATE создание и наполнение репозиториев
GOOGLE_CALENDAR_ID идентификатор календаря; пусто = интеграция выключена
GOOGLE_CREDENTIALS_FILE JSON сервис-аккаунта; лежит в data/, в git не уезжает
GOOGLE_CALENDAR_SYNC_MINUTES как часто делать полную сверку с календарём
TZ_DEFAULT часовой пояс по умолчанию для новых хакатонов
WEB_PORT, WEB_PUBLIC_URL порт и внешний адрес для ссылок в .ics
CARD_REFRESH_SECONDS как часто перерисовывать закреплённую карточку
STICKER_SET, STICKER_CHANCE пак стикеров и вероятность; 0 выключает
BANTER_CHANCE шанс влезть в чужой разговор; 0 выключает
BANTER_COOLDOWN_SECONDS, BANTER_CONTEXT пауза между репликами и глубина контекста
BANTER_EVERYWHERE false — оживать только в темах с хакатоном; по умолчанию работает везде
REACTION_CHANCE шанс повесить эмодзи на сообщение; 0 выключает
REACTION_COOLDOWN_SECONDS пауза между реакциями в одной теме

Устройство

src/hackbot/
├── config.py          настройки из .env
├── db/                модели и сессии SQLAlchemy
├── domain/
│   ├── enums.py       типы этапов, статусы, виды ссылок
│   ├── timeutils.py   разбор дат и русское форматирование
│   └── services/      вся бизнес-логика
├── agent/             pydantic-ai: извлечение с афиш, ReAct, генератор шуток
├── render/            сборка сообщений Telegram
├── bot/               тонкие хендлеры aiogram
├── scheduler/         цикл напоминаний, дайджестов и бэкапов
└── web/               FastAPI: страница таймлайна и .ics

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

Времена в базе всегда UTC, в часовой пояс хакатона переводит только слой отрисовки — из-за этого переход на летнее время нигде не участвует в арифметике.

Деплой

sudo cp deploy/hackbot.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now hackbot
journalctl -u hackbot -f

База лежит в data/hackbot.db, копия снимается раз в сутки в data/backups/ и хранится две недели.

About

Telegram bot that runs a hackathon timeline inside a forum topic: deadlines, reminders, LLM poster parsing, docs and GitHub

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages