Этот бот предназначен для студенческих чатов и личного использования. Он объединяет LLM-ассистента, автоматические поздравления и работу с расписанием занятий.
OurMate — это умный Telegram‑бот на базе aiogram и внешней LLM.
Работает на aiogram 3.26.0 и Telegram Bot API 9.5.
Поддерживает работу через HTTP/SOCKS5‑прокси для Telegram Bot API (Для macOS рекомендуется использовать Python, установленный через Homebrew, чтобы избежать проблем с сертификатами).
Он умеет:
- Отвечать на упоминания и личные сообщения с контекстом предыдущих диалогов (LLM)
- Ежедневно в заданное время проверять JSON-файл с днями рождения и поздравлять в группе
- При запуске бота уведомлять владельца в личку о ближайшем дне рождения
- Автоматически тянуть расписание через JSON-API, нормализовать тип занятия (Лекция/Практика/Лаб./Экзамен/Зачёт) и отдавать пары на сегодня/завтра командами или рассылкой
- Уведомлять чат об изменениях расписания (добавлено/убрано/изменено) и о появлении новой сессии
- Отвечать на вопросы о расписании на естественном языке и искать в интернете — через инструменты LLM (function calling), см. ниже
- Ставить напоминания на естественном языке («напомни в пятницу в 18 про созвон»): в беседе — с кнопкой «Подписаться / Отписаться» для подписчиков; в ЛС — персонально
- Предоставлять владельцу расширенные административные команды для контроля сервера и бота
📺 Для работы с LLM используется прямой ключ DeepSeek (endpoint https://api.deepseek.com/v1/chat/completions).
- Обрабатывает упоминания и личные сообщения:
chat.pyроутит вchat_pm.py/chat_group.py, команды вынесены вchat_commands.py, стрим-логика вllm_flow.py - Запоминает три последние пары вопрос–ответ (в ЛС — бессрочно, в группе — 24 часа); контекст не путается между разными чатами, в группе — общий для всех участников
- Использует внешнюю LLM для генерации ответов, с вежливым фолбэком и алертом владельцу при сбоях
- Стримит ответы с плейсхолдером в группах всегда, с троттлингом редактирования (группы ~1.8s/140 символов, ЛС ~0.6s/50) чтобы не ловить flood control; при
retry_afterждёт и догружает
2. Планировщик поздравлений
- Читает
data/birthdays.jsonи каждый день в заданное время шлёт поздравление в беседу (только если сегодня есть именинники); после поздравления пишет владельцу, кто следующий именинник - Исключает дубли в один день через кеш-файл
- Поздравляет именинников по шаблону из
.env - При запуске бота присылает владельцу уведомление о ближайшем дне рождения
3. Расписание пар (автообновление через JSON-API)
- Тянет расписание из публичного JSON-API (
/api/v1/ruz/scheduler/<group_id>?date=YYYY-MM-DD), сохраняет вdata/<code>/schedule.json. Поддерживает несколько групп — по подпапке на каждую - Перед каждой утренней рассылкой и ночным обновлением закрепа бот тянет свежие данные на текущую + N будущих недель (по умолчанию 3); сетевые ошибки 5xx/таймауты — один retry, при полном провале остаётся прошлый снимок
- Diff: после обновления, если расписание изменилось — бот шлёт в чат сообщение со списком 🆕 новых / ✅ заменивших слот / ❌ удалённых / ⏰ перенесённых по времени / ✏️ изменённых (место/тип) пар по датам, обёрнутое в цитату (с подписью «для
<code>» когда групп >1) - В multi-group режиме одинаковые пары двух групп выводятся одним блоком, а различающиеся — двумя
❗️ ... для <имя группы>блоками подряд - Команды «пары» / «пары завтра» (в группе, при упоминании бота или ответе; в ЛС — для владельца и пользователей из списка дней рождения); перед ответом — lazy-refresh с TTL (по умолчанию 60 мин), чтобы не дёргать API на каждый запрос
- «обнови расписание» — ручное принудительное обновление: показывает diff либо «расписание не изменилось», при недоступности API — гиперссылку на frontend и время прошлого снимка
- Если на выбранный день пар нет, бот показывает ближайшие будущие пары
- Ежедневная рассылка пар на сегодня в 08:00, только если пары есть (можно отключить флагом)
- Закреп: при включении бот держит закреплённое сообщение со списком следующих
PINNED_SCHEDULE_DAYS_AHEADучебных дней (по умолчанию 7)
- Ежедневное обновление закреплённого сообщения с парами
- Обновление запускается сразу после старта бота, не дожидаясь планового времени
- Если ближайших пар нет — закреп удаляется автоматически
- Утренняя рассылка может быть включена/выключена флагом, чтобы использовать либо закреп, либо рассылку
- В обычном диалоге модель сама решает — ответить текстом или вызвать функцию (родной паттерн tool use поверх OpenAI-совместимого API). Каркас тулов —
src/bot/services/llm_tools.py(ToolRegistry/run_tool_loop), стриминг с тул-вызовами —llm_service.py/llm_flow.py - Расписание на естественном языке: «что у нас в пятницу?», «во сколько последняя пара?» — tool
get_schedule(date_from, date_to); поиск занятий по предмету (прошлые и будущие) —find_classes_by_subject(subject)(schedule_tools.py). Текущая дата/таймзона инжектятся system-сообщением, поэтому «завтра» / «в субботу» резолвятся в конкретные даты - Веб-поиск: tool
web_search(query)ходит в интернет не сам, а через сторонний поисковый сервис Tavily — ключ в.env. Никакая LLM в сеть напрямую не ходит - Доступ к тулам расписания гейтится так же, как команды (в группе разрешён refresh+diff, в ЛС — нет)
- LLM-движок — тоже внешний сервис: DeepSeek (endpoint
https://api.deepseek.com/v1/chat/completions)
- Создаются и управляются на естественном языке через LLM-тулы: «напомни в пятницу в 18 про созвон», «покажи мои напоминания», «отмени напоминание про созвон»
- Четыре тула:
create_reminder/list_reminders/update_reminder/cancel_reminder(src/bot/services/reminder_tools.py) - Команда
напоминания— детерминированный компактный список активных напоминаний; доступна всем (какhelp/команды) - В беседе: бот постит карточку напоминания с единой кнопкой «Подписаться / Отписаться» — любой участник тогглит личную подписку, результат приходит персональным всплывающим уведомлением, а в карточке обновляется счётчик участников. Автор сразу подписан на своё напоминание (счётчик стартует с 1). В момент срабатывания все подписчики упоминаются в ответном сообщении
- В личных сообщениях: персональное напоминание с шагом подтверждения; доступно всем пользователям, которые писали боту
- Хранение в SQLite (
data/reminders.db); база переживает рестарт контейнера - При старте бота пропущенные напоминания (просроченные не более чем на
REMINDER_MISFIRE_HOURSчасов) досылаются с пометкой «опоздало»; более старые молча помечаются выполненными - При старте же чистятся завершённые/отменённые/неподтверждённые записи старше
REMINDER_RETENTION_DAYSдней (активные не трогаются)
Переменные окружения:
| Переменная | По умолчанию | Описание |
|---|---|---|
REMINDER_DB_PATH |
data/reminders.db |
Путь к SQLite-базе напоминаний |
REMINDER_MISFIRE_HOURS |
24 |
Окно (часов) для досылки просроченного напоминания при рестарте; старше — молча закрываются |
REMINDER_RETENTION_DAYS |
7 |
Срок хранения завершённых/отменённых/черновых записей; чистка при старте |
7. Пинг-лист (список для уведомлений)
- Opt-in список на беседу: люди сами вступают и могут позвать друг друга, когда в Telegram нет встроенного «уведомить подписавшихся»
- Команда
пинг(в беседе) постит панель со счётчиком участников и кнопками Вступить / Выйти (зелёная/красная черезstyle); нажатие тоглит участие и обновляет счётчик персональным всплывающим уведомлением — никого не пингует - Триггер
@all(обычным сообщением, без упоминания бота) зовёт всех из списка — упоминания черезtg://user?id=(работает и без username), длинные списки бьются на батчи - Кулдаун на
@all— 5 минут на беседу (в памяти), пустой список — без кулдауна - Выбывшие из беседы убираются из списка автоматически (апдейты
chat_member+ бэкап на сервисное сообщение о выходе) - Хранение в SQLite (
data/ping.db), отдельный список на каждую беседу - Требует у бота выключенный Group Privacy (чтобы видеть
@allбез упоминания) и права админа беседы (чтобы получать апдейты участников)
Переменные окружения:
| Переменная | По умолчанию | Описание |
|---|---|---|
PING_DB_PATH |
data/ping.db |
Путь к SQLite-базе пинг-листа |
PING_COOLDOWN_SECONDS |
300 |
Кулдаун на @all в секундах (на беседу) |
8. Гибкая конфигурация через .env
- Все токены и ключи хранятся в одном файле
- Промпты для чата и поздравлений можно менять без правки кода
- Установка времени для уведомлений
- Поддержка прокси для Telegram Bot API
- Логирование — единый формат
%(asctime)s [%(levelname)-8s] %(name)s: %(message)sдля всех строк (наш код + aiogram + httpx). Уровень настраивается переменнойLOG_LEVEL(по умолчаниюINFO). - Контекст — запоминает 3 пары диалога (в ЛС бессрочно, в группах 24 часа)
- Премиум-эмодзи — описываются в
src/core/emoji.py(классE);PremiumEmojiMiddlewareна исходящих сообщениях автоматически заменяет unicode на<tg-emoji>теги. В хэндлерах пишите обычный текст с эмодзи — middleware сам подставит ID. - Уведомления владельца — алерты о новых /start и о недоступности LLM
- Безопасность — проверки прав доступа для команд владельца
Группы доступа:
- 🌍 все;
- 🗿 владелец;
- 🎓 участники в беседе;
- 🎂 пользователи из
birthdays.json; - 🔒 только ЛС.
| Команда | Кто может воспользоваться | Описание |
|---|---|---|
help / команды |
🌍 | Справка по командам бота |
отписаться |
🔒 🎂 🗿 | Отключить поздравления (в ЛС, для пользователей из списка) |
др |
🎓 🎂 🗿 | Ближайший день рождения |
др 12345 / др @username |
🎓 🎂 🗿 | Дата дня рождения по id или username |
пары |
🎓 🎂 🗿 | Пары на сегодня |
пары завтра |
🎓 🎂 🗿 | Пары на завтра |
обнови расписание |
🎓 🗿 | Принудительное обновление расписания и закрепа (в беседе; в ЛС — только владелец) |
пинг |
🎓 🗿 | Панель списка для уведомлений (вступить/выйти кнопками); только в беседе |
@all |
🎓 | Позвать всех из списка (в беседе, без упоминания бота) |
напоминания |
🌍 | Список активных напоминаний |
logs |
🗿 | Краткие логи бота (только строки PM/GR/FP) |
full logs |
🗿 | Последние 200 строк лога целиком |
проверка ссылок |
🗿 | Диагностика ссылок и активации пользователей |
Команды
stop bot/status/systemудалены после переезда на Docker — для остановки и статуса используйтеmake stop/make ps/make tailна сервере.
- В беседе все команды (включая
help/команды) работают только при упоминании бота или ответе на его сообщение — без этого бот молчит. - В ЛС команды, кроме
help/команды, доступны только владельцу и пользователям изdata/birthdays.json. - Чтобы получать поздравления, активируйте бота в ЛС (напишите любое сообщение).
- Для повторной активации после отписки снова напишите боту любое сообщение.
OurMate_bot/
├── src/ # Основной код приложения
│ ├── bot/ # Логика Telegram бота
│ │ ├── setup.py # Сборка Bot/Dispatcher + middleware
│ │ ├── handlers/ # Обработчики сообщений
│ │ │ ├── chat.py # Роутер: триггер-гейт + dispatch PM/GR
│ │ │ ├── access.py # Единый гейтинг доступа (classify/resolve)
│ │ │ ├── chat_pm.py / chat_group.py # Обработка ЛС / групп (стрим + фолбэк)
│ │ │ ├── chat_commands.py # Публичные команды (др/пары/обнови расписание)
│ │ │ ├── owner_commands.py # Команды владельца (logs/проверка ссылок)
│ │ │ ├── llm_flow.py # Стриминг LLM-ответов + рендер (тул-флоу)
│ │ │ └── … # chat_context, commands, errors, placeholder_variants
│ │ ├── middlewares/ # Middleware aiogram
│ │ │ └── emoji.py # PremiumEmojiMiddleware: unicode → <tg-emoji>
│ │ ├── services/ # Бизнес-логика
│ │ │ ├── llm_service.py # LLM API + стрим с тулами
│ │ │ ├── llm_tools.py # Каркас function calling (ToolRegistry/run_tool_loop)
│ │ │ ├── schedule_tools.py # Тулы расписания (get_schedule, find_classes_by_subject)
│ │ │ ├── web_search_tool.py # Тул web_search через сторонний Tavily
│ │ │ ├── reminder_tools.py # Тулы напоминаний (create/list/update/cancel)
│ │ │ ├── reminder_service.py / reminder_store.py # Бизнес-логика и SQLite-хранилище
│ │ │ ├── schedule_*.py # Пайплайн расписания: schedule_client/schedule_parser/diff/refresher/service
│ │ │ └── *_service.py # birthday / context / system
│ │ └── scheduler/ # Cron-задачи: поздравления, рассылка/закреп/автообновление расписания, напоминания
│ ├── core/ # Доменное ядро (emoji.py — класс E: unicode + premium_id)
│ ├── models/ # Модели данных (user.py)
│ ├── utils/ # Хелперы: логи, даты, HTML-рендер, кеш get_me, текст
│ └── config/ # settings.py — настройки из .env
├── data/ # Данные приложения (не в git)
│ ├── birthdays.json # Дни рождения
│ ├── reminders.db # SQLite-база напоминаний (путь задаётся REMINDER_DB_PATH)
│ ├── <CODE>/schedule.json # Снимок расписания из JSON-API (подпапка на группу)
│ └── cache/ # Кеш: дедуп поздравлений, расписание, message_id закрепа
├── main.py # Точка входа (тонкий entrypoint)
├── docker-compose.yml / Dockerfile # Контейнер bot и сборка образа
├── Makefile # Цели для разработки и эксплуатации
├── requirements.txt # Зависимости Python
├── .env.example # Шаблон переменных окружения (.env — реальный, не в git)
└── README.md # Документация
Бот запускается через Docker Compose + Makefile.
1. Клонировать репозиторий
git clone https://github.com/Naz1anmak/OurMate.git
cd OurMatemake env # копирует .env.example → .env, если его нетЗатем открой .env и заполни обязательные значения: BOT_TOKEN, OWNER_CHAT_ID, CHAT_ID, LLM_API_KEY, прокси (если нужен), промпты PROMPT_TEMPLATE_*.
Список переменных с подсказками — в .env.example. Переменная LOG_LEVEL управляет уровнем логов (DEBUG/INFO/WARNING/ERROR, по умолчанию INFO).
3. Сборка и запуск через Docker Compose
make build # собрать образ
make up # запустить контейнер bot в фоне
make tail # хвост логов с follow (Ctrl+C — выйти)Все цели Make — make help:
| Цель | Действие |
|---|---|
make help |
список целей |
make env |
создать .env из .env.example |
make up / make down |
запустить / остановить и удалить контейнеры |
make stop |
остановить без удаления |
make restart |
пересобрать и перезапустить |
make build |
только пересобрать образ |
make logs |
прицепиться ко всем логам |
make tail |
последние 200 строк лога bot с follow |
make ps |
список контейнеров compose |
make sh |
войти в контейнер bot |
Структура JSON-файла с пользователями:
{
"users": [
{
"user_id": 123456789,
"name": "Анастасия Ильинична",
"last_name": "Алленова",
"birthday": "30.12",
"status": "active",
"interacted_with_bot": false,
"username": "example_user"
},
{
"user_id": 987654321,
"name": "Иван Петрович",
"last_name": "Петров",
"birthday": "10.7",
"status": "-",
"interacted_with_bot": false
}
]
}Поля:
user_id(int или null) — Telegram user_id для упоминаний и гиперссылокname(str) — Имя и отчество пользователяlast_name(str) — Фамилия (используется для отчётности)birthday(str) — Дата рождения в формате "D.M" или "DD.MM"status(str) — Статус: "active" (действующий студент) или "-" (отчисленный)interacted_with_bot(bool) — Активировал ли пользователь бота (по умолчанию false)username(str, опционально) — Telegram username (обновляется автоматически при старте бота)
Важно об активации:
Чтобы пользователь получал поздравления в беседе с гиперссылкой на его профиль, он должен активировать бота:
- Написать боту /start в личные сообщения
- После этого поле
interacted_with_botавтоматически станетtrue - Пользователь будет получать персональные поздравления от LLM в беседе
Если interacted_with_bot = false, бот не будет поздравлять этого пользователя в беседе.
Команда для отписки:
- Пользователь может в любой момент отписаться от поздравлений, написав боту
отписатьсяв ЛС - Для повторной активации нужно снова написать боту
5. Расписание (автообновление через JSON-API)
Бот сам тянет расписание из публичного JSON-API портала расписания — никаких ручных .ics не нужно. Подключение групп:
- В
.envуказатьSCHEDULE_API_BASE_URL(домен портала расписания твоего вуза, со схемойhttps://) иSCHEDULE_API_FACULTY_ID(числовой ID факультета из URL). - Для каждой группы добавить
SCHEDULE_API_GROUP_<CODE>=<group_id>, где<CODE>— твой код подпапки (любой удобный, латиница/цифры), а<group_id>— числовой ID из URL страницы группы (между/groups/и?date=). - Запустить бот — подпапка
data/<CODE>/создастся при первом обновлении и наполнитсяschedule.json.
Пример .env:
SCHEDULE_AUTO_UPDATE_ENABLED=true
SCHEDULE_API_BASE_URL=https://<домен-портала-расписания>
SCHEDULE_API_FACULTY_ID=125
SCHEDULE_API_GROUP_<CODE1>=<ID1>
SCHEDULE_API_GROUP_<CODE2>=<ID2>
SCHEDULE_API_WEEKS_AHEAD=3 # текущая + 3 будущих недели
SCHEDULE_API_HTTP_TIMEOUT=15 # секунд
SCHEDULE_API_LAZY_TTL_MIN=60 # TTL для lazy-refresh в командахКогда обновляется.
- Перед рассылкой (
SCHEDULE_SEND_HOUR:MM) —force_refresh+ при изменениях шлёт diff, затем пары на актуальный день: «Пары на сегодня», а если все сегодняшние пары уже прошли (напр. вечерняя рассылка) — «Пары на завтра» (как/парыи закреп). Если в актуальный день пар нет — рассылка молчит. - Перед обновлением закрепа (
PINNED_SCHEDULE_UPDATE_HOUR:MM) —force_refresh+ diff, затем рендер закрепа. - В командах «пары» / «пары завтра» —
ensure_freshс TTL: если последний снимок свежееSCHEDULE_API_LAZY_TTL_MIN, не дёргаем API. - По команде «обнови расписание» — принудительный
force_refresh+ обновление закрепа.
Что в diff. Сообщение «🗓️ Расписание обновилось» содержит блоки по датам (содержимое блока обёрнуто в <blockquote>, как в /пары и закрепе). Формат строки пары — <emoji> HH:MM–HH:MM · Тип, на следующей строке предмет жирным. Эмодзи:
- 🆕 — новая пара (в этом слоте раньше ничего не было);
- ✅ — добавленная пара, занявшая слот удалённой (замена предмета в то же время начала);
- ❌ — удалённые;
- ⏰ — изменилось только время (тот же предмет, тот же тип/место);
- ✏️ — изменилось место и/или тип.
Если у нескольких групп на одну дату полностью совпадает состояние до и после, блок объединяется в один — и при полном покрытии всех известных групп суффикс «для …» опускается; иначе в заголовке появляется «для 40001 и 40002» (через _format_groups).
При появлении расписания «с нуля» (новая сессия после пустого периода) — короткий заголовок «🗓️ Появилось расписание!» без перечисления (содержимое всё равно придёт в утренней рассылке/закрепе).
Отображаемое имя группы. SCHEDULE_GROUP_NAME_PREFIX + код подпапки. Пример: при SCHEDULE_GROUP_NAME_PREFIX=prefix/ подпапка A отображается как prefix/A. Совпадающие пары двух групп показываются одним блоком; различающиеся — двумя блоками с заголовком ❗️ ... для <имя группы>.
Лимит закрепа. PINNED_SCHEDULE_DAYS_AHEAD (по умолчанию 7) ограничивает число ближайших учебных дней в закреплённом сообщении, чтобы оно не разрасталось при загрузке расписания на семестр.
Сеть недоступна? Если все недели для группы не загрузились — старый снимок остаётся (fetched_at не сдвигается). На команде «обнови расписание» бот ответит «
Префиксы помогают быстро понимать источник события:
- PM — Personal Message (личные сообщения)
- GR — Group Reply (ответы в группе)
- FP — First Ping (команды
/start)
Формат задаётся src/utils/logging.py::configure_logging и одинаков для нашего кода и сторонних библиотек:
2026-05-14 12:34:56 [INFO ] src.bot.handlers.commands: FP; От @user (Имя): /start
Уровень управляется переменной LOG_LEVEL (INFO по умолчанию). Для подробной диагностики стрим-логики удобно временно ставить LOG_LEVEL=DEBUG.
- В файл
data/logs/bot.log(RotatingFileHandler, до 10 МБ × 5 файлов). Это bind-volume, логи переживают рестарты контейнера. - Параллельно весь вывод идёт в stdout —
make tail/make logsпоказывают ровно то же самое через docker-driver.
logs— краткие строки с маркерами PM/GR/FP.full logs— последние 200 строк лога целиком.
Обе обрезаются до 4000 символов Telegram-лимита.
- Команды владельца работают только для указанного
OWNER_CHAT_ID - Команды владельца (
logs,full logs,проверка ссылок) читают локальный файл логов /birthdays.json, не дёргают системные утилиты - Ограничение длины ответов (4000 символов)
- Проверка типов чатов: команды владельца доступны в ЛС и группе только владельцу; публичные команды в ЛС — только для владельца и пользователей из
birthdays.json
🔄 Развертывание на сервере
1. Установить Docker и Docker Compose (Ubuntu/Debian)
# Базовые пакеты и репозиторий Docker
sudo apt update && sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg git make
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
. /etc/os-release
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/$ID $VERSION_CODENAME stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# Запустить и включить автостарт
sudo systemctl enable --now docker
# Проверка
docker --version
docker compose version(Опционально) Разрешить запуск docker без sudo:
sudo usermod -aG docker $USER
newgrp docker # или перелогинитьсяgit clone https://github.com/Naz1anmak/OurMate.git bot && cd bot
make env # создаст .env из .env.example
$EDITOR .env # заполнить значения (BOT_TOKEN, OWNER_CHAT_ID, CHAT_ID, LLM_API_KEY, промпты)
mkdir -p data/logs && sudo chown -R 1000:1000 data # ⚠️ см. ниже
make build && make up
make tail # убедиться, что появилась строка «Бот запущен и готов к работе»Права на
data/. Внутри контейнера бот работает под пользователемapp(UID 1000), а./dataмонтируется как bind-volume с хоста. Если папка принадлежитroot, бот не сможет создатьdata/logs/bot.log([Errno 13] Permission denied: 'data/logs'вmake tail) и не сможет обновлятьdata/birthdays.json. Один раз выполнитьsudo chown -R 1000:1000 dataрешает проблему навсегда. Тот же фикс актуален, если ты впервые обновляешься со старой схемы (логи через systemd/journald) — папкаdata/logs/появилась только в текущей версии.
Контейнер автоматически перезапускается при сбое (restart: unless-stopped в docker-compose.yml). Логи сервиса смотрите через make tail или make logs; они же дублируются в data/logs/bot.log (10 МБ × 5 файлов ротации), что переживает рестарт контейнера.
3. Обновление после изменений в репозитории
cd /root/bot
git pull
make restart # пересборка образа + перезапуск
make tailПосле make up запусти make tail и проверь, что появились строки:
… [INFO ] __main__: Запуск бота...
… [INFO ] src.bot.setup: Обработчики зарегистрированы
… [INFO ] __main__: DeleteWebhook успешно выполнен
… [INFO ] __main__: Планировщики запущены
… [INFO ] __main__: Бот запущен и готов к работе
Затем напиши боту /start в ЛС — в логе должен появиться FP; От … /start, а в твоей личке владельца — уведомление о новом активации.
Если в make tail всплыло [WARNING ] root: Не удалось открыть data/logs/bot.log для логов: [Errno 13] Permission denied: 'data/logs' — папка data/ принадлежит не UID 1000. Выполните sudo chown -R 1000:1000 data и make restart.