Image

DIY: свой бесплатный чат-бот в Telegram

a51ad72dce05e3af1fc61955efe0d0a7

В этой статье я покажу, как буквально за 15 минут создать собственного Telegram чат-бота на базе ИИ и начать использовать его абсолютно бесплатно.

56955cd8e8dac1857ac772a06a88e73b

Пока продолжается работа над основным проектом, который я курирую (ссылка), хочу поделиться приятным опытом экспериментов с микроконтроллером ESP32 на примере платы Lolin Lite.
Не скрою — эта модель мне особенно симпатична благодаря компактности, достаточному числу портов ввода-вывода и хорошему запасу по производительности.

Надеюсь, эта статья окажется полезной и для вас.

Начало

Для реализации проекта потребуется выполнить несколько шагов:

  1. Зарегистрировать бота в Telegram

  2. Создать аккаунт на openrouter.ai

  3. Внести изменения в прошивку

  4. Залить прошивку на микроконтроллер

Регистрация бота в Telegram

1fd1a93de199b4315ed375bc2728a435

Через поиск в Telegram находим @BotFather (без кавычек) и открываем диалог.

Отправляем команду /newbot — после этого BotFather предложит ввести несколько параметров:

  1. Название бота
    Укажите публичное имя — то, что будут видеть пользователи (например, SmartHome Assistant).

  2. Юзернейм (адрес) бота
    Это уникальное имя, которое будет заканчиваться на bot (например, smarthome_helper_bot). Если имя занято, BotFather попросит ввести другое.

  3. Получение API-токена
    После успешного создания BotFather выдаст токен доступа (API key). Сохраните его — он потребуется позже.

  4. Готово
    Бот зарегистрирован, можно переходить к следующему шагу.

Регистрация в OpenRouter

Шаги регистрации и получения API-ключа:

77fd4f76291296e05a1a3c8ad9b1073a

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

  1. Переходим на сайт openrouter.ai и регистрируемся. Можно авторизоваться через Google, GitHub или с помощью почты.

  2. После входа открываем: Settings → API Keys → Create API Key.

  3. В поле Name указываем любое название (для себя) и нажимаем Create.

  4. Появится ваш API key — сохраните его, он понадобится далее.

  5. Затем снова переходим в Settings и открываем раздел Training, Logging, & Privacy. Включаем второй переключатель напротив пункта: Enable free endpoints that may publish prompts.

  6. Этап завершён.

Код для микроконтроллера

Далее я выкладываю сам код, который будет необходимо немного настроить:

Скрытый текст// ESP32 Telegram LLM BOT by RealZel #include <WiFi.h> #include <WiFiClientSecure.h> #include <HTTPClient.h> #include <ArduinoJson.h> #include «time.h» // ====== Настройки ====== const char* ssid = «Название точки доступа Wi-Fi»; const char* password = «Пароль точки доступа Wi-Fi»; const char* telegramBotToken = «Сюда ваш API с Botfather Telegram»; const char* telegramApiBase = «https://api.telegram.org/bot»; const char* LLM_API_URL = «https://openrouter.ai/api/v1/chat/completions»; const char* LLM_MODEL = «deepseek/deepseek-chat-v3.1:free»; const char* LLM_API_KEY = «Сюда ваш API с OpenRouter»; const char* ntpServer = «pool.ntp.org»; const long gmtOffset_sec = 2 * 3600; const int daylightOffset_sec = 0; const unsigned long POLL_INTERVAL_MS = 2000; const size_t TELEGRAM_MSG_LIMIT = 3800; // ====== Persona / системный промпт ====== const char* systemPrompt = «При получении команды /start в своем сообщении сообщай, что ты текстовый помощник и готов ответить на вопросы. Среднее время ответа до 30 секунд.n» «Не строй таблицы.n» «Каждый ответ должен умещаться максимум в два сообщения Telegram (~7600 символов).n» «Если текст длиннее, сокращай, сохраняя ключевую и самую важную информацию.n» «Делай текст связным и понятным, избегай обрезки важных предложений.n» «Размер одного сообщения не должен превышать 3800 символов.n»; // ====== Глобальные переменные ====== WiFiClientSecure client; long lastUpdateId = 0; unsigned long lastPoll = 0; // ====== Хранение предыдущих сообщений ====== String lastUserMessage = «»; String lastBotMessage = «»; // ====== Вспомогательные функции ====== void setupTime() { configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); time_t now = time(nullptr); int tries = 0; while (now < 1600000000 && tries < 20) { delay(500); now = time(nullptr); tries++; } } String httpGet(const String &url) { HTTPClient https; if (url.startsWith(«https://»)) https.begin(client, url); else https.begin(url); int code = https.GET(); String payload = «»; if (code > 0) payload = https.getString(); https.end(); return payload; } String httpPostJsonWithAuth(const String &url, const String &body, const char* bearer) { HTTPClient https; if (url.startsWith(«https://»)) https.begin(client, url); else https.begin(url); https.addHeader(«Content-Type», «application/json»); if (bearer && strlen(bearer) > 0) { String auth = String(«Bearer «) + bearer; https.addHeader(«Authorization», auth); } int code = https.POST(body); String payload = «»; if (code > 0) payload = https.getString(); https.end(); return payload; } // ====== Отправка сообщений в Telegram с делением на части ====== void sendTelegramMessage(long chat_id, const String &text, long reply_to_message_id = 0) { size_t start = 0; size_t len = text.length(); while (start < len) { String chunk = text.substring(start, start + TELEGRAM_MSG_LIMIT); start += TELEGRAM_MSG_LIMIT; StaticJsonDocument<2000> doc; doc[«chat_id»] = chat_id; doc[«text»] = chunk; doc[«parse_mode»] = «HTML»; if (reply_to_message_id) doc[«reply_to_message_id»] = reply_to_message_id; String body; serializeJson(doc, body); httpPostJsonWithAuth(String(telegramApiBase) + telegramBotToken + «/sendMessage», body, nullptr); } } // ====== Формирование тела запроса к LLM с учетом предыдущих сообщений ====== String buildLLMRequestBody(const String &userMessage) { StaticJsonDocument<8192> req; req[«model»] = LLM_MODEL; req[«temperature»] = 0.7; req[«max_tokens»] = 1200; // ограничиваем, чтобы LLM сам пытался уложиться в 2 сообщения JsonArray messages = req.createNestedArray(«messages»); // Системный промпт JsonObject m0 = messages.createNestedObject(); m0[«role»] = «system»; m0[«content»] = systemPrompt; // Предыдущее сообщение пользователя и ответ бота if (lastUserMessage.length() > 0 && lastBotMessage.length() > 0) { JsonObject prevUser = messages.createNestedObject(); prevUser[«role»] = «user»; prevUser[«content»] = lastUserMessage; JsonObject prevBot = messages.createNestedObject(); prevBot[«role»] = «assistant»; prevBot[«content»] = lastBotMessage; } // Текущее сообщение пользователя JsonObject currentUser = messages.createNestedObject(); currentUser[«role»] = «user»; currentUser[«content»] = userMessage; String out; serializeJson(req, out); return out; } // ====== Универсальный разбор ответа LLM ====== String parseLLMResponse(const String &resp) { if (resp.length() == 0) return «Ответа нет, попробуйте еще раз.»; StaticJsonDocument<20000> doc; DeserializationError err = deserializeJson(doc, resp); if (err) return «Ошибка разбора JSON от LLM.»; if (doc.containsKey(«choices»)) { JsonArray choices = doc[«choices»].as<JsonArray>(); if (choices.size() > 0) { JsonObject c0 = choices[0].as<JsonObject>(); if (c0.containsKey(«message») && c0[«message»].containsKey(«content»)) return String((const char*)c0[«message»][«content»]); if (c0.containsKey(«text»)) return String((const char*)c0[«text»]); if (c0.containsKey(«content»)) return String((const char*)c0[«content»]); } } if (doc.containsKey(«text»)) return String((const char*)doc[«text»]); return «LLM вернул неожиданный формат ответа.»; } String callLLM(const String &userMessage) { String body = buildLLMRequestBody(userMessage); String resp = httpPostJsonWithAuth(String(LLM_API_URL), body, LLM_API_KEY); String result = parseLLMResponse(resp); return result; } // ====== Фильтры ====== bool containsDangerous(const String &text) { String low = text; low.toLowerCase(); const char* forbids[] = {«убить», nullptr}; for (int i=0; forbids[i]!=nullptr; ++i) if (low.indexOf(forbids[i]) != -1) return true; return false; } bool looksLikeMedicalPrescription(const String &text) { String low = text; low.toLowerCase(); const char* meds[] = {«», nullptr}; for (int i=0; meds[i]!=nullptr; ++i) if (low.indexOf(meds[i]) != -1) return true; return false; } String postFilterResponse(const String &reply) { if (reply.length() == 0) return «Нейросеть молчит, попробуйте позже.»; if (looksLikeMedicalPrescription(reply)) return «Не даю медицинских рекомендаций. Обратитесь к врачу.»; if (containsDangerous(reply)) return «На такие опасные инструкции не отвечаю.»; return reply; } // ====== Telegram Updates ====== void processUpdates() { String url = String(telegramApiBase) + String(telegramBotToken) + «/getUpdates?timeout=5»; if (lastUpdateId) url += «&offset=» + String(lastUpdateId + 1); String resp = httpGet(url); if (resp.length() == 0) return; StaticJsonDocument<20000> doc; if (deserializeJson(doc, resp)) return; if (!doc.containsKey(«result»)) return; for (JsonVariant update : doc[«result»].as<JsonArray>()) { if (update.containsKey(«update_id»)) lastUpdateId = update[«update_id»].as<long>(); if (!update.containsKey(«message»)) continue; JsonObject msg = update[«message»].as<JsonObject>(); long chat_id = msg[«chat»][«id»].as<long>(); long message_id = msg[«message_id»].as<long>(); if (!msg.containsKey(«text»)) { sendTelegramMessage(chat_id, «Пишите текст, я не умею обрабатывать медиа.», message_id); continue; } String text = String((const char*)msg[«text»]); if (containsDangerous(text)) { sendTelegramMessage(chat_id, «На такие запросы не могу ответить.», message_id); continue; } String hfReply = callLLM(text); String safeReply = postFilterResponse(hfReply); // Сохраняем контекст для следующего запроса lastUserMessage = text; lastBotMessage = safeReply; sendTelegramMessage(chat_id, safeReply, message_id); } } // ====== setup / loop ====== void setup() { Serial.begin(115200); WiFi.begin(ssid, password); int tries = 0; while (WiFi.status() != WL_CONNECTED && tries < 60) { delay(500); Serial.print(«.»); tries++; } if (WiFi.status() == WL_CONNECTED) { Serial.println(«WiFi connected, IP: » + WiFi.localIP().toString()); } else Serial.println(«WiFi connection failed»); setupTime(); client.setInsecure(); lastUpdateId = 0; } void loop() { if (millis() — lastPoll >= POLL_INTERVAL_MS) { lastPoll = millis(); processUpdates(); } }

В коде необходимо указать четыре параметра:

  • SSID и пароль вашей Wi-Fi сети

  • API-токен Telegram (из BotFather)

  • API-ключ OpenRouter

  • Persona / системный промт (необязательно)

Что делает прошивка сейчас:

  • Принимает сообщения от пользователя и, в соответствии с инструкциями из блока Persona, отправляет их в OpenRouter на модель deepseek-chat-v3.1.

  • Получает ответ от модели и отправляет его в Telegram.

  • Автоматически делит длинные ответы на несколько сообщений.

  • Позволяет менять характер бота: достаточно изменить текст в разделе Persona.

  • Имеет встроенные фильтры: если сообщение содержит запрещённые слова, бот сразу отправляет уведомление, что не может отвечать на такие запросы.

  • Запоминает предыдущее сообщение в переписке, поддерживая контекст общения.

  • При желании можно использовать любую другую LLM на OpenRouter — достаточно изменить значение в строке LLM_MODEL.

Пример ответа на сообщения
Пример ответа на сообщения

В результате всех шагов у вас будет собственный Telegram-бот, который будет отвечать на сообщения.

Важно понимать, что память ESP32 ограничена, и устройство стабильно обрабатывает запросы только от одного пользователя. Поэтому текущая реализация подходит прежде всего для личных экспериментов и хобби-проектов.

Впрочем, есть и практичные сценарии: вы можете привязать бота к своему каналу и запрограммировать его, например, на публикацию сообщений по расписанию или по событиям.

Вот и всё на сегодня. Надеюсь вам было интересно и полезно.

Источник: habr.com

✅ Найденные теги: DIY:, новости
Каталог бесплатных опенсорс-решений, которые можно развернуть локально и забыть о подписках

галерея

Фото сгенерированных лиц: исследование показывает, что люди не могут отличить настоящие лица от сгенерированных
Нейросети построили капитализм за трое суток: 100 агентов Claude заперли…
Скетч: цифровой осьминог и виртуальный мир внутри компьютера с человечком.
Сцена с жестами пальцами, где один жест символизирует "VPN", а другой "KHP".
‼️Paramount купила Warner Bros. Discovery — сумма сделки составила безумные…
Скриншот репозитория GitHub "Claude Scientific Skills" AI для научных исследований.
Структура эффективного запроса Claude с элементами задачи, контекста и референса.
Эскиз и готовая веб-страница платформы для AI-дизайна в современном темном режиме.
ideipro logotyp
Image Not Found
Звёздное небо с галактиками и туманностями, космос, Вселенная, астрофотография.

Система оповещения обсерватории Рубина отправила 800 000 сигналов в первую ночь наблюдений.

Астрономы будут получать оповещения о небесных явлениях в течение нескольких минут после их обнаружения. Теренс О'Брайен, редактор раздела «Выходные». Публикации этого автора будут добавляться в вашу ежедневную рассылку по электронной почте и в ленту новостей на главной…

Мар 2, 2026
Женщина с длинными тёмными волосами в синем свете, нейтральный фон.

Расследование в отношении 61-фунтовой машины, которая «пожирает» пластик и выплевывает кирпичи.

Обзор компактного пресса для мягкого пластика Clear Drop — и что будет дальше. Шон Холлистер, старший редактор Публикации этого автора будут добавляться в вашу ежедневную рассылку по электронной почте и в ленту новостей на главной странице вашего…

Мар 2, 2026
Черный углеродное волокно с текстурой плетения, отражающий свет.

Материал будущего: как работает «бессмертный» композит

Учёные из Университета штата Северная Каролина представили композит нового поколения, способный самостоятельно восстанавливаться после серьёзных повреждений.  Речь идёт о модифицированном армированном волокном полимере (FRP), который не просто сохраняет прочность при малом весе, но и способен «залечивать» внутренние…

Мар 2, 2026
Круглый экран с изображением замка и горы, рядом электронная плата.

Круглый дисплей Waveshare для креативных проектов

Круглый 7-дюймовый сенсорный дисплей от Waveshare создан для разработчиков и дизайнеров, которым нужен нестандартный экран.  Это IPS-панель с разрешением 1 080×1 080 пикселей, поддержкой 10-точечного ёмкостного сенсора, оптической склейкой и защитным закалённым стеклом, выполненная в круглом форм-факторе.…

Мар 2, 2026

Впишите свой почтовый адрес и мы будем присылать вам на почту самые свежие новости в числе самых первых