Telegram уведомления в Nuxt 4: от простых алертов до интерактивной модерации

Когда ваш блог или веб-приложение начинает жить своей жизнью — появляются новые пользователи, публикуются статьи, приходят комментарии — хочется быть в курсе происходящего. Email-уведомления работают, но кто сейчас проверяет почту в реальном времени? Telegram — другое дело: мгновенные пуши, всегда под рукой, и можно даже отвечать на действия прямо из чата.
В этой статье я расскажу, как я интегрировал Telegram в свой Nuxt 4 блог: от базовых уведомлений о событиях до полноценной модерации комментариев с inline-кнопками — и всё это без единой сторонней библиотеки.
Что мы реализуем
- Уведомления о событиях — логин, регистрация, новая статья, новый комментарий
- Интерактивная модерация — кнопки "Одобрить / Отклонить / Спам" прямо в Telegram
- Webhook для обратной связи — обработка нажатий на кнопки
Стек: Nuxt 4, MongoDB/Mongoose, нативный fetch (без axios и telegram-библиотек)
Часть 1: Создание Telegram бота
Прежде чем писать код, нужно создать бота и получить его токен.
Шаг 1: Создание бота через @BotFather
- Откройте Telegram и найдите @BotFather
- Отправьте команду
/newbot - Введите имя бота (например, "My Blog Notifications")
- Введите username бота (должен заканчиваться на
bot, напримерmyblog_alerts_bot) - Сохраните полученный токен — он выглядит как
123456789:ABCdefGHIjklMNOpqrsTUVwxyz
Шаг 2: Получение Chat ID
Чтобы бот мог отправлять вам сообщения, нужен ваш Chat ID:
- Напишите что-нибудь вашему новому боту
- Откройте в браузере:
https://api.telegram.org/bot<TOKEN>/getUpdates - Найдите в ответе
"chat":{"id": 123456789}— это ваш Chat ID
Для группового чата: добавьте бота в группу, напишите сообщение и снова проверьте getUpdates.
Шаг 3: Настройка переменных окружения
Добавьте в файл .env:
TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz
TELEGRAM_CHAT_ID=123456789
TELEGRAM_WEBHOOK_SECRET=any_random_string_for_security
И зарегистрируйте их в nuxt.config.ts:
nuxt.config.ts — runtimeConfig
export default defineNuxtConfig({
runtimeConfig: {
// Серверные переменные (не доступны на клиенте)
mongodbUri: process.env.MONGODB_URI || 'mongodb://localhost:27017',
jwtSecret: process.env.JWT_SECRET || 'your-secret-key',
// Telegram
telegramBotToken: process.env.TELEGRAM_BOT_TOKEN || '',
telegramChatId: process.env.TELEGRAM_CHAT_ID || '',
telegramWebhookSecret: process.env.TELEGRAM_WEBHOOK_SECRET || '',
public: {
siteUrl: process.env.SITE_URL || 'http://localhost:3000',
}
},
// ...
})
Часть 2: Утилита для отправки сообщений
Создадим универсальную утилиту для работы с Telegram API. Ключевые принципы:
- Нативный fetch — не тянем лишние зависимости
- Graceful degradation — если Telegram недоступен, приложение продолжает работать
- Non-blocking — уведомления не замедляют основные операции
Базовая структура
Создайте файл server/utils/telegram.ts:
Полный код telegram.ts
/**
* Telegram notification utility for admin alerts
* Uses native fetch (Node 18+) - no external dependencies needed
* All functions are non-blocking and never throw errors to the caller
*/
interface TelegramConfig {
botToken: string
chatId: string
siteUrl: string
}
/**
* Get Telegram configuration from runtime config
*/
function getTelegramConfig(): TelegramConfig | null {
const config = useRuntimeConfig()
if (!config.telegramBotToken || !config.telegramChatId) {
return null
}
return {
botToken: config.telegramBotToken as string,
chatId: config.telegramChatId as string,
siteUrl: config.public.siteUrl as string
}
}
/**
* Escape HTML special characters to prevent injection
*/
function escapeHtml(text: string): string {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
}
/**
* Format timestamp in Moscow timezone
*/
function getTimestamp(): string {
return new Date().toLocaleString('ru-RU', { timeZone: 'Europe/Moscow' })
}
/**
* Base function to send message via Telegram Bot API
*/
export async function sendTelegramMessage(text: string): Promise<void> {
const config = getTelegramConfig()
if (!config) return
const url = `https://api.telegram.org/bot${config.botToken}/sendMessage`
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: config.chatId,
text: text,
parse_mode: 'HTML',
disable_web_page_preview: true
})
})
if (!response.ok) {
const error = await response.text()
console.warn('[Telegram] Failed to send message:', error)
}
} catch (error) {
console.warn('[Telegram] Network error:', error)
}
}
/**
* Send message with inline keyboard
*/
export async function sendTelegramMessageWithKeyboard(
text: string,
replyMarkup: object
): Promise<void> {
const config = getTelegramConfig()
if (!config) return
const url = `https://api.telegram.org/bot${config.botToken}/sendMessage`
try {
await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: config.chatId,
text,
parse_mode: 'HTML',
disable_web_page_preview: true,
reply_markup: replyMarkup
})
})
} catch (error) {
console.warn('[Telegram] Error:', error)
}
}
/**
* Edit existing message (remove buttons after action)
*/
export async function editTelegramMessage(
messageId: number,
chatId: string,
newText: string
): Promise<void> {
const config = getTelegramConfig()
if (!config) return
const url = `https://api.telegram.org/bot${config.botToken}/editMessageText`
try {
await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: chatId,
message_id: messageId,
text: newText,
parse_mode: 'HTML'
})
})
} catch (error) {
console.warn('[Telegram] Edit error:', error)
}
}
/**
* Answer callback query (acknowledge button press)
*/
export async function answerCallbackQuery(
callbackQueryId: string,
text: string
): Promise<void> {
const config = getTelegramConfig()
if (!config) return
const url = `https://api.telegram.org/bot${config.botToken}/answerCallbackQuery`
try {
await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
callback_query_id: callbackQueryId,
text
})
})
} catch (error) {
console.warn('[Telegram] Callback error:', error)
}
}
Функции уведомлений
Добавим специализированные функции для каждого типа события:
Уведомление о логине
export async function notifyLogin(user: {
name: string
email: string
role: string
}): Promise<void> {
const config = getTelegramConfig()
if (!config) return
const roleEmoji = user.role === 'admin' ? '👑' : '👤'
const timestamp = getTimestamp()
const message = `
🔐 <b>Вход в систему</b>
${roleEmoji} <b>Пользователь:</b> ${escapeHtml(user.name)}
📧 <b>Email:</b> ${escapeHtml(user.email)}
🏷️ <b>Роль:</b> ${user.role}
🕐 <b>Время:</b> ${timestamp}
`.trim()
await sendTelegramMessage(message)
}
Уведомление о регистрации
export async function notifyRegistration(user: {
name: string
email: string
}): Promise<void> {
const config = getTelegramConfig()
if (!config) return
const timestamp = getTimestamp()
const message = `
🎉 <b>Новый пользователь</b>
👤 <b>Имя:</b> ${escapeHtml(user.name)}
📧 <b>Email:</b> ${escapeHtml(user.email)}
🕐 <b>Время:</b> ${timestamp}
<a href="${config.siteUrl}/admin/users">Управление пользователями</a>
`.trim()
await sendTelegramMessage(message)
}
Уведомление о новой статье
export async function notifyNewArticle(article: {
title: string
slug: string
status: string
author: { name: string }
}): Promise<void> {
const config = getTelegramConfig()
if (!config) return
const statusEmoji = article.status === 'published' ? '✅' : '📝'
const statusText = article.status === 'published' ? 'Опубликована' : 'Черновик'
const timestamp = getTimestamp()
const articleUrl = article.status === 'published'
? `${config.siteUrl}/articles/${article.slug}`
: `${config.siteUrl}/admin/articles`
const message = `
📄 <b>Новая статья</b>
${statusEmoji} <b>Статус:</b> ${statusText}
📰 <b>Заголовок:</b> ${escapeHtml(article.title)}
✍️ <b>Автор:</b> ${escapeHtml(article.author.name)}
🕐 <b>Время:</b> ${timestamp}
<a href="${articleUrl}">Открыть статью</a>
`.trim()
await sendTelegramMessage(message)
}
Уведомление о комментарии с кнопками модерации
export async function notifyNewComment(
comment: {
id: string
content: string
author: { name: string }
},
article: {
title: string
slug: string
}
): Promise<void> {
const config = getTelegramConfig()
if (!config) return
const timestamp = getTimestamp()
// Обрезаем длинные комментарии
const maxLength = 200
const truncatedContent = comment.content.length > maxLength
? comment.content.substring(0, maxLength) + '...'
: comment.content
const message = `
💬 <b>Новый комментарий</b> (на модерации)
📰 <b>Статья:</b> ${escapeHtml(article.title)}
👤 <b>Автор:</b> ${escapeHtml(comment.author.name)}
🕐 <b>Время:</b> ${timestamp}
<i>"${escapeHtml(truncatedContent)}"</i>
`.trim()
// Inline-кнопки для модерации
const inlineKeyboard = {
inline_keyboard: [[
{ text: '✅ Одобрить', callback_data: `comment:approve:${comment.id}` },
{ text: '❌ Отклонить', callback_data: `comment:reject:${comment.id}` },
{ text: '🚫 Спам', callback_data: `comment:spam:${comment.id}` }
]]
}
await sendTelegramMessageWithKeyboard(message, inlineKeyboard)
}
Часть 3: Интеграция в API эндпоинты
Теперь подключим уведомления к существующим эндпоинтам. Важный момент: вызываем функции без await, чтобы не блокировать основной ответ.
Логин
// server/api/auth/login.post.ts
import { notifyLogin } from '../../utils/telegram'
export default defineEventHandler(async (event) => {
// ... валидация и аутентификация ...
// Сохраняем refresh token
user.refreshToken = refreshToken
await user.save()
// Telegram уведомление (non-blocking)
notifyLogin({ name: user.name, email: user.email, role: user.role })
return { user: { ... }, token: accessToken, refreshToken }
})
Регистрация
// server/api/auth/register.post.ts
import { notifyRegistration } from '../../utils/telegram'
export default defineEventHandler(async (event) => {
// ... создание пользователя ...
// Telegram уведомление (non-blocking)
notifyRegistration({ name: user.name, email: user.email })
return { user: { ... }, token: accessToken, refreshToken }
})
Создание статьи
// server/api/articles/index.post.ts
import { notifyNewArticle } from '../../utils/telegram'
export default defineEventHandler(async (event) => {
const user = await requireAdmin(event)
// ... создание статьи ...
// Telegram уведомление (non-blocking)
notifyNewArticle({
title: article.title,
slug: article.slug,
status: article.status,
author: { name: user.name }
})
return article
})
Создание комментария
// server/api/comments/index.post.ts
import { notifyNewComment } from '../../utils/telegram'
export default defineEventHandler(async (event) => {
const user = await requireAuth(event)
// ... создание комментария ...
// Telegram уведомление с кнопками модерации (non-blocking)
notifyNewComment(
{ id: comment._id.toString(), content: data.content, author: { name: user.name } },
{ title: article.title, slug: article.slug }
)
return { comment: populatedComment, message: 'Комментарий отправлен на модерацию' }
})
Часть 4: Webhook для интерактивной модерации
Самая интересная часть — обработка нажатий на кнопки. Telegram отправляет POST-запрос на наш webhook при каждом нажатии.
Создание webhook эндпоинта
server/api/telegram/webhook.post.ts
import { Comment } from '../../models/Comment'
import { editTelegramMessage, answerCallbackQuery } from '../../utils/telegram'
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
// Проверка секретного токена от Telegram
const secretToken = getHeader(event, 'x-telegram-bot-api-secret-token')
if (config.telegramWebhookSecret && secretToken !== config.telegramWebhookSecret) {
throw createError({ statusCode: 403, statusMessage: 'Forbidden' })
}
const body = await readBody(event)
// Обрабатываем только callback_query (нажатия на кнопки)
if (!body.callback_query) {
return { ok: true }
}
const { callback_query } = body
const { data, message, id: callbackId } = callback_query
// Парсим callback_data: "comment:action:commentId"
const [type, action, commentId] = data.split(':')
if (type !== 'comment' || !commentId) {
await answerCallbackQuery(callbackId, 'Неизвестное действие')
return { ok: true }
}
// Маппинг действий на статусы
const statusMap: Record<string, string> = {
approve: 'approved',
reject: 'rejected',
spam: 'spam'
}
const newStatus = statusMap[action]
if (!newStatus) {
await answerCallbackQuery(callbackId, 'Неизвестное действие')
return { ok: true }
}
// Обновляем статус комментария в БД
const comment = await Comment.findByIdAndUpdate(
commentId,
{ status: newStatus },
{ new: true }
)
if (!comment) {
await answerCallbackQuery(callbackId, 'Комментарий не найден')
return { ok: true }
}
// Информация о статусе для ответа
const statusEmoji: Record<string, string> = {
approved: '✅',
rejected: '❌',
spam: '🚫'
}
const statusText: Record<string, string> = {
approved: 'Одобрен',
rejected: 'Отклонён',
spam: 'Спам'
}
// Обновляем сообщение (убираем кнопки, добавляем результат)
const originalText = message.text
const updatedText = `${originalText}\n\n${statusEmoji[newStatus]} <b>Статус:</b> ${statusText[newStatus]}`
await editTelegramMessage(message.message_id, message.chat.id, updatedText)
await answerCallbackQuery(callbackId, `Комментарий ${statusText[newStatus].toLowerCase()}`)
return { ok: true }
})
Регистрация webhook в Telegram
После деплоя приложения нужно зарегистрировать URL webhook. Это делается одноразовым запросом:
curl -X POST "https://api.telegram.org/bot<YOUR_TOKEN>/setWebhook" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-domain.com/api/telegram/webhook",
"secret_token": "your_TELEGRAM_WEBHOOK_SECRET"
}'
Успешный ответ:
{"ok":true,"result":true,"description":"Webhook was set"}
Локальная разработка с ngrok
Для тестирования на localhost нужен туннель. Используйте ngrok:
# Установка
brew install ngrok
# Запуск туннеля
ngrok http 3000
ngrok выдаст публичный URL вида https://xxxx-xxx-xxx.ngrok-free.app. Используйте его для регистрации webhook.
Важно: При каждом перезапуске ngrok URL меняется — нужно перерегистрировать webhook.
Часть 5: Форматирование сообщений
Telegram поддерживает HTML и Markdown форматирование. Мы используем HTML (parse_mode: 'HTML'):
| Тег | Результат |
|---|---|
<b>текст</b> | жирный |
<i>текст</i> | курсив |
<code>текст</code> | моноширинный |
<a href="url">текст</a> | ссылка |
<pre>блок</pre> | блок кода |
Экранирование HTML
Обязательно экранируйте пользовательский ввод, чтобы избежать инъекций:
function escapeHtml(text: string): string {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
}
// Использование
const message = `Пользователь: ${escapeHtml(user.name)}`
Часть 6: Почему без библиотек?
Возможно, вы задаётесь вопросом: почему не использовать node-telegram-bot-api или telegraf?
Когда достаточно нативного fetch
- Отправка сообщений в одну сторону (сервер → Telegram)
- Обработка простых callback-запросов
- Минимальное количество методов API (sendMessage, editMessageText, answerCallbackQuery)
Когда нужны библиотеки
- Полноценный бот с командами (
/start,/help) - Сложные диалоги и состояния (FSM)
- Работа с файлами, стикерами, голосовыми
- Long polling вместо webhook
- Middleware и плагины
Преимущества минималистичного подхода
- Нет лишних зависимостей — меньше потенциальных уязвимостей
- Меньший размер бандла — важно для serverless
- Полный контроль — понимаете каждую строку кода
- Легко расширить — добавить новый метод API — это 10 строк
Архитектура решения
┌─────────────────────────────────────────────────────────────────┐
│ Nuxt Application │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ login.post │ │ register │ │ comments/index.post │ │
│ │ │ │ .post │ │ │ │
│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
│ │ │ │ │
│ └──────────────────┼──────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ server/utils/ │ │
│ │ telegram.ts │ │
│ │ │ │
│ │ - sendTelegramMsg │ │
│ │ - notifyLogin │ │
│ │ - notifyNewComment │ │
│ │ - editMessage │ │
│ └──────────┬──────────┘ │
│ │ │
└────────────────────────────┼───────────────────────────────────┘
│
▼
┌─────────────────────┐
│ Telegram Bot API │
│ api.telegram.org │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Admin's Telegram │
│ Chat / Group │
└──────────┬──────────┘
│
Нажатие кнопки
│
▼
┌─────────────────────┐
│ /api/telegram/ │
│ webhook.post.ts │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ MongoDB │
│ Update comment │
│ status │
└─────────────────────┘
Чеклист для деплоя
- Создан бот через @BotFather
- Получен Chat ID
- Добавлены переменные в
.env:TELEGRAM_BOT_TOKENTELEGRAM_CHAT_IDTELEGRAM_WEBHOOK_SECRET
- Переменные прописаны в
nuxt.config.ts - Создан
server/utils/telegram.ts - Создан
server/api/telegram/webhook.post.ts - Интегрированы вызовы в API эндпоинты
- Зарегистрирован webhook после деплоя
- Протестированы все типы уведомлений
Заключение
За несколько часов работы мы получили полноценную систему уведомлений:
- Мгновенные алерты о всех важных событиях
- Интерактивная модерация без захода в админку
- Ноль зависимостей — только нативный fetch
- Graceful degradation — если Telegram недоступен, приложение работает
Это решение легко расширить: добавить уведомления о других событиях, отправлять сообщения разным пользователям, интегрировать более сложные сценарии взаимодействия.
Telegram Bot API предоставляет множество возможностей — от простых сообщений до полноценных мини-приложений. Начните с простого и расширяйте по мере необходимости.
Комментарии
Войдите, чтобы оставить комментарий
Пока нет комментариев. Будьте первым!