История моего первого телеграм бота
13 июня 2026 г.
На третьем курсе Бийского Государственного Колледжа я был счастливым обладателем одного телегам бота, который отправлял в чат расписание этого учебного заведения.
На пике популярности ботом пользовалось чуть больше 1000 человек. Это был мой первый проект, который подарил мне опыт реальной production разработки.
Но как я вообще пришел к такой идее? Что же побудило меня взять на себя эту ношу?
Глава 1 “Бийский Государственный Колледж”
Я не знаю как дела обстояли в других учебных заведениях, но так исторически сложилось, что в Бийском Государственном Колледже (далее БГК) никто не знал расписание дальше одного дня. Так называемые “замены” с новым расписанием на день выходили каждый день. Эти замены публиковались на сайте. Проверять наличие новой замены было крайне неудобно. Особенно с телефона. Нужно было:
- Зайти на сайт и скачать расписание (публиковалось в виде
.xlsxфайла). - Открыть гигантскую таблицу и найти там кусочек со своей группы.
- Повторять цикл на протяжении дня, потому что расписание никогда не публиковали в одно и то же время + его могли за день поменять несколько раз.
Местные умельцы среди студентов решили проблему, написав ВК бота. Этот бот был в каждом групповом чате и он делал очень простую вещь: отправлял сообщение с фотографией расписания, когда оно обновлялось на сайте. Это было очень удобно:
- Не нужно заниматься имитацией Short Pulling-а, периодически скачивая расписание с сайта. Если оно там обновилось, то бот сразу об этом оповестит.
- Не нужно беспокоиться, что расписание обновится несколько раз за день, потому что опять же бот будет оповещать о каждом таком изменении.
- Не нужно копаться в таблице с телефона (это неудобно). Бот сразу отправляет изображение, которое можно зазумить на нужном участке с расписанием своей группы.
Это был не мой бот. Я сам пользовался этим даром цивилизации и даже не подозревал, что когда-то мне придется составлять ему конкуренцию 😅.
Но в один зимний вечер все изменилось.
Глава 2 “Я навсегда ушел из VK”
Когда-то давно у меня был аккаунт в социальной сети ВКонтакте, состояние которой стремительно продолжало ухудшаться после того, как в 2014 году из компании ушел Павел Дуров.
Всяческие редизайны, непопулярные механики и лагающее мобильное приложение вполне можно было затерпеть, как я и делал долгие годы. Так бы все и продолжалось, если бы контент сайта не превратился в сплошной поток рекламы.
В начале 2022 года, листая ленту, я устал перелистывать рекламу. Интереса ради решил подсчитать соотношение реальных постов с новостями/мемами к рекламным постам и очень удивился.
Лента ВК на тот момент состояла из рекламы на 2/3. 1/3 — реклама самой социальной сети + 1/3 — реклама в пабликах, которые я читаю.
И я решил прибегнуть к радикальным мерам, т.е. удалить свою страницу во ВКонтакте и полностью перебраться в Telegram, которым я уже пользовался с 2016 года.
Проблем почти не было. Все нужные мне люди уже были в Telegram, а ежедневные порции мемов мне поставляли местные каналы и Pikabu.
И все было бы хорошо, если бы не одно “НО”. На тот момент, уход в телегу означал для меня добровольный отказ от бота, который отправляет расписание БГК в чате нашей группы. Это была слишком нужная вещь, чтобы просто отказаться от такого.
И я решил создать своего бота, но в Telegram!
Глава 3 “БГК ScheduleBot”
Бот был написан мной на C#. Для работы с API Telegram-а я использовал соответствующий .NET Client для Telegram Bot API.
Архитектурно программа представляла собой консольное приложение, которое параллельно запускало 2 процесса:
- Обработка бота (реакция на команды пользователей).
- Поиск расписания колледжа.
На втором процессе можно остановиться подробнее:
- В бесконечном цикле с небольшими интервалами выполняется проверка расписания.
- Эта проверка скачивает таблицу с расписанием с сайта колледжа.
- Новая скачанная таблица сравнивается по размеру файла со старой таблицей (всегда хранилась одна последняя скачанная таблица).
- Если таблица такая же как и была, то выходим из поиска и ждем интервал до следующей проверки.
- Если таблица новая, то преобразуем ее в изображение.
- Отправляем в Telegram это изображение всем подписчикам.
Пользователи подписывались на бота, отправляя ему соответствующую команду. После чего TelegramId сохранялся в БД с пометкой номера корпуса, чтобы отправлять изменения только интересующего расписания.
Преобразование таблицы в изображение происходило через внешний сервис CloudConverter, который предоставлял возможность бесплатной конвертации нескольких таблиц в день. Мне этого более чем хватало.
Запускалось все это чудо на моем компьютере, который я включал каждое утро, чтобы запустить бота. Все это ради того, чтобы где-то в обед получить картинку с новым расписанием, пока я учился на другом конце города.
Глава 4 “Эволюция бота за 1,5 года его жизни”
Бот идеальным сразу не получился. На протяжении всего своего существования он подвергался разным улучшениям. Эта глава расскажет про различные трудности с которыми я сталкивался и как их решал.
Переход на VPS
Первым делом мне надоело, что нужно держать свой компьютер включенным, чтобы бот мог работать круглосуточно.
Изучая тему, я понял, что аренда VPS это то что мне нужно. Сравнив различные тарифы у самых разных хостеров, я в свое время остановился на RuVDS, который тогда предоставлял самый слабый конфиг (1 ядро / 512 Mb ОЗУ / 10 Gb HDD) за скромные 130 руб. в месяц. При моей стипендии в 840 рублей я приобрел этот сервер без лишних раздумий.
С этого момента началось мое знакомство с Linux и первые потуги в администрирование подобной системы, но сейчас не об этом…
Доставлял я свое приложение буквально “руками по FTP”, потому что не знал о других способах. В целом весь мой навык DevOps тех лет сводился к этому мему, который для меня тогда скорее был руководством к действию.

Безобразие с доставкой файлов на сервер “руками” длилось не очень долго. Ровно до тех пор, пока мой друг не рассказал мне что такое Docker (привет, Тимофей 👋).
Мой сервер взломали через PostgreSQL
Будучи столь неопытным в работе с Docker, я допустил фатальную ошибку. Я открыл наружу порты на контейнере с PostgreSQL. Первые пару недель идея казалась весьма привлекательной. Я мог зайти в свою БД через DBeaver/DataGrip и наблюдать за постоянно растущими данными, которые верно давали понять — число подписчиков бота растет!
Счастье длилось недолго, потому что в один день бот перестал работать. Опытным путем удалось выяснить, что если убить контейнер с СУБД вместе с файлами, то на чистую базу приложение стартует идеально. Файлы старой базы я решил скачать себе на компьютер для детального изучения проблемы. В моей системе Windows Defender сразу же отрапортовал, что в файлах БД находится троян, майнер и бекдор…
Порты на контейнере с СУБД были моментально закрыты. Больше никакой сканирующий бот/скрипт не мог подключиться из сети к моей СУБД и перебором подобрать стандартный логин/пароль postgres/1234.
Я ускорил отправку расписания в несколько раз
С каждым днем бот становился все популярнее. Вместе с тем появлялись новые проблемы. Например, из-за того, что бот рассылал своим подписчикам картинки по очереди, то, если в одной группе есть два подписчика на расписание своего корпуса, которые подписались на бота в разное время (из-за чего между ними в очереди много других подписчиков), то первый товарищ получал расписание на пару минут быстрее своего однокурсника.
Код отправки изображения расписания подписчикам выглядел примерно вот так:
await using var db = new DataBaseProvider();
var subscribers = db.Subscribers.Where(x => x.Corps == (int)corps).ToArray();
foreach (var subscriber in subscribers)
await SendSchedulePictureAsync(subscriber.TelegramId, corps);
Решая эту проблему, я впервые в своей практике пришел к тому, что можно дожидаться завершения Task не сразу, а складывать их в массив, чтобы после дождаться выполнения всей пачки разом.
await using var db = new DataBaseProvider();
var subscribers = db.Subscribers.Where(x => x.Corps == (int)corps).ToArray();
var tasks = new List<Task>();
foreach (var subscriber in subscribers)
tasks.Add(SendSchedulePictureAsync(subscriber.TelegramId, corps));
await Task.WhenAll(tasks);
Так я добился параллельной отправки расписания. Процесс отправки ускорился и стал занимать считанные секунды, а не минуты.
Починил падение бота, при отправке расписания пользователю, который заблокировал бота
Знали ли вы, что попытка отправки сообщения пользователю (как минимум в том пакете с реализацией API для C#) генерирует исключение, если пользователь заблокировал бота (но предварительно подписался на расписание)?
Я в документации к библиотеке этого не нашел, а узнал по факту того, что бот не отправил расписание, когда оно обновилось на сайте. По логам понял, что приложение упало при попытке отправить на один конкретный TelegramId. Причину падения выяснил очень быстро, потому что я лично знал человека с таким идентификатором и просто спросил у него как он пользуется ботом :D.
После этого код отправки стал таким. Я просто начал удалять из базы пользователей, которым не удается отправить сообщение. В моем понимании они более не являлись моими реальными подписчиками.
await using var db = new DataBaseProvider();
var subscribers = db.Subscribers.Where(x => x.Corps == (int)corps).ToArray();
var tasks = new List<Task>();
foreach (var subscriber in subscribers)
{
try
{
tasks.Add(SendSchedulePictureAsync(subscriber.TelegramId, corps));
}
catch
{
await RemoveSubscriberAsync(subscriber.TelegramId);
}
}
await Task.WhenAll(tasks);
Переход с PostgreSQL на SQLite
В конце концов пользоваться такой “жирной” СУБД для моей задачи стало бессмысленно:
- Мне не нужна возможность удаленно подключаться к БД, чтобы считать подписчиков, потому что позже я сделал команду для бота, которая отправляет статистику сразу в чат.
- У меня не такая большая нагрузка.
- Приложение можно сократить до одного Docker контейнера, что упростит процесс деплоя (SQLite это встраиваемая СУБД, которой не нужен отдельный контейнер).

Замена CloudConverter на LibreOffice
В какой-то момент бесплатных лимитов конвертации стало не хватать (когда я их все потратил, пока дебажил очередной баг с отправкой) и пришлось начать искать альтернативы.
Альтернативы были или платные (сразу нет) или оставляли жирные ватермарки на все изображение.
Поиски зашли настолько глубоко, что я прознал, что у LibreOffice есть CLI через который можно запросить экспортировать таблицу в изображение.
Провернул это следующим образом: сначала я установил сам LibreOffice в Docker образ своего приложения, дописав пару строк в Dockerfile:
RUN apt-get update
RUN apt-get install -y libreoffice
А потом дописал код в приложение, который выполняет консольную команду:
// Раньше этот метод делал HTTP запрос в CloudConverter
private static async Task GetSchedulePictureAsync(Corps corps)
{
using var process = new Process
{
StartInfo =
{
FileName = "/bin/bash",
Arguments =
$"-c \"cd /app/Data ; libreoffice --convert-to jpg {GetOldTablePath(corps)} --headless\"",
RedirectStandardInput = true,
RedirectStandardOutput = true
}
};
process.Start();
await process.WaitForExitAsync();
}
После выполнения команды изображение с расписанием сохранялось на диск, как и раньше.
Это дало несколько преимуществ:
- Можно сколько угодно тестировать отправку расписания с реальными отправками, не беспокоясь за какие-либо лимиты на конвертацию.
- Повысилась отказоустойчивость системы, потому что больше нет зависимости от CloudConverter.
UI на RazorPages
В какой-то момент я захотел полноценную админку для своего бота, чтобы конфигурировать его через UI.
Без лишних подробностей просто покажу как это выглядело:

Разумеется это далеко не все изменения. Например, часть багов не содержит под собой какой-то особенной истории, чтобы много про них писать. Среди таких:
- Бот мог упасть, если получал сообщение о запросе расписания до того как расписание появлялось в памяти (первый запуск после чистки диска, когда в очереди сообщений уже есть команда от пользователя).
- Бот не мог подписать пользователя на обновление расписания, потому что считал, что он уже подписан из-за некорректной проверки подписки.
- Бот мог упасть, если у сайта колледжа заканчивался срок действия SSL сертификата.
И все же одно важное улучшение я намеренно упустил из вида, чтобы посвятить ему отдельную главу!
Глава 5 “Я начал логировать все сообщения пользователей (и смешно и грустно)”
Одна из фичей моего бота была в том, что он в текстовый файл сохранял все сообщения, которые получал от пользователей. В первую очередь мне это было необходимо, чтобы расследовать различные проблемы и баги.
Какое-то время я мог получать все эти текстовые файлы обычным архивом прямо с телефона, а позже добавил возможность читать логи прямо в UI (после перехода на RazorPages).
Сначала логи использовались по прямому назначению, но очень скоро это все превратилось в полноценную юмористическую рубрику, где самые отборные акты шизофрении я рассылал всем своим друзьям, чтобы вместе похихикать.
P.S. Читать сверху вниз.

Подборка смешных сообщений в бота










Это все были логи из текстового варианта. Дальше пойдут более красивые. Следующие скрины были сделаны, когда у бота появился UI.
P.S. Эти надо читать уже снизу вверх (на UI другая сортировка логов).








Еще был забавный случай, когда сайт колледжа сломался, и с сайта начали скачиваться какие-то таблицы с рандомным расписанием. У каждого корпуса расписание сменилось за 1 час более 10 раз. На каждое такое обновление бот отправлял картинку каждому подписчику.
Пользователи были очень “благодарны” за этот великолепный спам расписаниями.





Заключение

Этот проект дал мне в свое время бесценный опыт разработки и замотивировал продолжать и дальше делать различные веб-приложения, в том или ином виде.
А еще он мне напоминает о временах, когда я был студентом и обновлял логи с приятным предвкушением, что сейчас опять поржу с какой-нибудь ерунды, которую написали в бот.
Одно время даже было желание реализовать возможность из админки писать сообщения в чаты от лица бота, но руки так и не дошли.
Этого бота я, по сути, делал для себя. И поэтому его история подошла к концу вместе с окончанием четвертого курса, а бот “конкурента” из ВК был отключен еще раньше. Таким образом, мне даже удалось какое-то время побыть монополистом.
Была у меня идея передать этого бота кому-нибудь в наследство со всеми инструкциями. Но желающих сопровождать этот код и платить 130 рублей в месяц за хостинг не нашлось.
Бот просуществовал с января 2022 года по май 2023 года. И с тех пор в БГК вновь настали темные времена ежедневного скачивания таблицы расписания с сайта.