Telegram-бот-напоминатель на PHP: команды, расписание и отправка по cron
Что соберём
Бот принимает команды в личном чате:
/remind 2026-11-07 19:30 Забрать посылку
/list
/cancel 3
Webhook сохраняет задачу в JSON-файл вне публичной папки сайта. Планировщик хостинга запускает PHP-скрипт раз в минуту; worker выбирает просроченные напоминания и отправляет их через Telegram Bot API.
Это самостоятельный учебный проект для небольшого бота. Он не использует сторонние PHP-пакеты, но требует PHP 8.1+, cURL, HTTPS и возможности запускать cron/планировщик. Для крупного сервиса JSON-хранилище следует заменить базой данных и очередью задач.
Архитектура
Пользователь Telegram
│ команда /remind
▼
HTTPS webhook ── проверка секрета ── ReminderBot
│
▼
JSON-хранилище
▲
│ due tasks
Планировщик ── PHP worker ───────────┘
│
└── Telegram Bot API: sendMessage
Здесь два отдельных процесса. Webhook принимает команду и быстро сохраняет её. CLI-worker запускается по расписанию и отправляет наступившие задачи. Так не приходится держать постоянно работающий PHP-процесс на обычном виртуальном хостинге.
1. Создаём бота и готовим PHP-хостинг
Создайте тестового бота через @BotFather и сохраните выданный токен в защищённой настройке хостинга. Для PHP нужны cURL и права записи на каталог данных. Веб-сервер должен направлять запросы только в public/, а JSON-файл храниться вне document root.
Пример структуры:
telegram-reminder-bot/
├── bin/
│ ├── send-due-reminders.php
│ └── set-webhook.php
├── public/
│ └── webhook.php
├── src/
│ ├── JsonReminderStore.php
│ ├── ReminderBot.php
│ ├── ReminderStore.php
│ ├── TelegramApi.php
│ └── TelegramClient.php
├── tests/run.php
└── var/ # вне публичной папки, создаётся при первом запуске
Переменные окружения:
TELEGRAM_BOT_TOKEN— секретный токен;TELEGRAM_WEBHOOK_SECRET— случайный секрет для проверки входящих POST;TELEGRAM_WEBHOOK_URL— HTTPS-адресpublic/webhook.php;BOT_TIMEZONE— часовой пояс расписания, по умолчаниюEurope/Moscow;REMINDER_DATA_FILE— необязательный полный путь к приватному JSON-файлу.
Секрет для webhook можно получить локально:
php -r "echo bin2hex(random_bytes(32)), PHP_EOL;"
На сервере настройте document root на public/. Каталог с данными вынесите за пределы публичной директории и предоставьте права записи пользователю PHP/cron. Токен не помещайте в исходный код или архив с проектом.
2. Формат и часовой пояс
Команда использует строго определённый формат:
/remind ГГГГ-ММ-ДД ЧЧ:ММ текст напоминания
Например:
/remind 2026-11-07 19:30 Забрать посылку
Время интерпретируется в BOT_TIMEZONE. Бот переводит его в UTC для хранения; worker также сравнивает время в UTC. Так расписание не зависит от того, в каком часовом поясе настроен сервер.
При разборе даты проверяются ошибки календаря и обратное форматирование: дата вроде 31 февраля не должна молча превращаться в март. Бот также отклоняет время менее чем через минуту от текущего. Длина текста ограничена 500 символами, а на один чат можно создать не более 100 активных напоминаний.
3. Храним задачи в JSON с блокировкой
JsonReminderStore реализует интерфейс ReminderStore. У напоминания хранятся:
{
"id": 3,
"chat_id": "123456789",
"run_at": "2026-11-07T16:30:00Z",
"text": "Забрать посылку",
"status": "pending",
"attempts": 0,
"next_attempt_at": "2026-11-07T16:30:00Z"
}
Перед изменением данных приложение блокирует отдельный lock-файл через flock(). После изменения оно записывает временный файл и заменяет основной атомарным rename(). Это не даёт двум одновременным webhook-запросам одновременно перезаписать весь файл и потерять запись.
Бот разрешает /list и /cancel ID только для владельца задачи: сравнивается chat_id из входящего сообщения. Дополнительно бот работает только в личном чате, не в группах. JSON-файл должен находиться вне web root, поскольку в нём хранятся тексты личных напоминаний.
4. Обрабатываем команды
ReminderBot получает зависимости через конструктор: Telegram-клиент, хранилище и часовой пояс. Это позволяет отдельно тестировать команды без сети.
Основной сценарий /remind:
- Проверить структуру команды и длину текста.
- Разобрать дату во временной зоне бота.
- Отклонить несуществующую дату или слишком близкое время.
- Преобразовать значение в UTC.
- Сохранить напоминание и вернуть его ID.
/list показывает до 20 ближайших активных задач. /cancel ID отменяет только активную задачу текущего чата. Если ID неизвестен или принадлежит другому пользователю, бот отвечает, что активное напоминание не найдено.
В проекте текст отправляется как обычный текст Telegram без HTML/Markdown-разметки. Поэтому пользовательский текст не интерпретируется как форматирующая разметка.
5. Регистрируем webhook
После загрузки файлов задайте переменные окружения в панели хостинга и запустите из корня проекта:
php bin/set-webhook.php
URL должен открываться по HTTPS с действительным сертификатом. Скрипт проверяет URL и формат секрета, после чего регистрирует webhook с разрешённым типом обновлений message.
Endpoint проверяет заголовок X-Telegram-Bot-Api-Secret-Token через hash_equals(), принимает только POST и ограничивает размер JSON до 1 МБ. Ошибки не раскрывают пользователю внутренние сведения сервера. Если обработка не удалась, сервер возвращает 500, чтобы Telegram мог повторить доставку.
6. Настраиваем отправку по расписанию
Worker — это отдельный CLI-скрипт:
php bin/send-due-reminders.php
Добавьте команду в планировщик хостинга с периодом одна минута. На Linux запись crontab может выглядеть так:
* * * * * /usr/bin/php /home/account/telegram-reminder-bot/bin/send-due-reminders.php
Путь к PHP и каталогу проекта замените на реальные значения сервера. Worker использует lock-файл, чтобы два запуска не обрабатывали один набор задач одновременно. Он выбирает до 20 напоминаний за один запуск.
Если Telegram API временно недоступен, попытки повторяются с задержками 2, 4, 8, 16 и 32 минуты. После пятой ошибки задача получает статус failed; причина записывается в серверный error log без токена и текста напоминания. Проверяйте журналы после первого запуска планировщика.
7. Тестируем без настоящего Telegram
Запустите:
php tests/run.php
Тесты создают временное JSON-хранилище, подставляют фальшивый Telegram API и проверяют:
- создание будущего напоминания;
- сохранение и чтение списка;
- изоляцию задач между чатами;
- запрет отмены чужого напоминания;
- отмену своей задачи.
После теста временные файлы удаляются. Для живой проверки отправьте тестовому боту /help, добавьте напоминание на 2–3 минуты вперёд и проверьте, что планировщик запускается. Не используйте для проверки важные личные данные.
8. Ограничения и переход к промышленному варианту
Файловое хранилище подходит для учебного проекта и небольшого личного бота. При высокой нагрузке, нескольких серверах или росте числа записей используйте MySQL/PostgreSQL с транзакциями и отдельную очередь задач. Так проще управлять резервными копиями, сроками хранения и конкурентной обработкой.
Есть важное ограничение доставки: если Telegram принял сообщение, а PHP-процесс завершился до отметки sent, при следующей попытке пользователь может получить дубликат. Внешний Bot API не предоставляет транзакцию, которая одновременно отправляет сообщение и фиксирует локальное состояние. Для напоминаний дубликат обычно допустим; для платежей и других критичных операций нужна отдельная архитектура с очередью/outbox и идемпотентной обработкой.
Входящие webhook-обновления тоже могут доставляться повторно при сбое ответа. Этот учебный проект не хранит обработанные update_id, поэтому не подключайте к нему операции, для которых повтор команды опасен. Для следующей версии добавьте таблицу обработанных обновлений с уникальным индексом и транзакцию создания напоминания.
Частые проблемы
Бот подтверждает задачу, но не присылает напоминание. Проверьте cron, путь к PHP CLI, REMINDER_DATA_FILE и права на каталог. Запустите worker вручную и проверьте серверный error log.
Команда с датой отклоняется. Проверьте точный формат и время: /remind 2026-11-07 19:30 Текст. Время уже прошло или наступит менее чем через минуту — бот его не примет.
Время отличается на несколько часов. Посмотрите значение BOT_TIMEZONE; по умолчанию это Europe/Moscow. Убедитесь, что планировщик и PHP используют тот же релиз файлов и окружение.
Нет доступа к JSON-файлу. Укажите путь вне document root, доступный пользователю PHP и cron. Не решайте проблему выдачей прав на запись всем пользователям сервера.
Итог
Получился законченный небольшой сервис: webhook, команды создания/просмотра/отмены, проверка ввода, файловое хранилище, worker по cron, повторные попытки и тесты. Архив исходников содержит готовую структуру проекта; в боевую среду его следует переносить только после настройки HTTPS, приватного каталога данных и тестового напоминания.
Скачать исходный код: архив проекта
KOTOFF.NET