Nuxt

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

BorisBoris
|
9 января 2026 г.
|
12 мин чтения
|
35 просмотров
Telegram уведомления в Nuxt 4: от простых алертов до интерактивной модерации

Когда ваш блог или веб-приложение начинает жить своей жизнью — появляются новые пользователи, публикуются статьи, приходят комментарии — хочется быть в курсе происходящего. Email-уведомления работают, но кто сейчас проверяет почту в реальном времени? Telegram — другое дело: мгновенные пуши, всегда под рукой, и можно даже отвечать на действия прямо из чата.

В этой статье я расскажу, как я интегрировал Telegram в свой Nuxt 4 блог: от базовых уведомлений о событиях до полноценной модерации комментариев с inline-кнопками — и всё это без единой сторонней библиотеки.

Что мы реализуем

  1. Уведомления о событиях — логин, регистрация, новая статья, новый комментарий
  2. Интерактивная модерация — кнопки "Одобрить / Отклонить / Спам" прямо в Telegram
  3. Webhook для обратной связи — обработка нажатий на кнопки

Стек: Nuxt 4, MongoDB/Mongoose, нативный fetch (без axios и telegram-библиотек)


Часть 1: Создание Telegram бота

Прежде чем писать код, нужно создать бота и получить его токен.

Шаг 1: Создание бота через @BotFather

  1. Откройте Telegram и найдите @BotFather
  2. Отправьте команду /newbot
  3. Введите имя бота (например, "My Blog Notifications")
  4. Введите username бота (должен заканчиваться на bot, например myblog_alerts_bot)
  5. Сохраните полученный токен — он выглядит как 123456789:ABCdefGHIjklMNOpqrsTUVwxyz

Шаг 2: Получение Chat ID

Чтобы бот мог отправлять вам сообщения, нужен ваш Chat ID:

  1. Напишите что-нибудь вашему новому боту
  2. Откройте в браузере: https://api.telegram.org/bot<TOKEN>/getUpdates
  3. Найдите в ответе "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, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
}

/**
 * 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, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
}

// Использование
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 и плагины

Преимущества минималистичного подхода

  1. Нет лишних зависимостей — меньше потенциальных уязвимостей
  2. Меньший размер бандла — важно для serverless
  3. Полный контроль — понимаете каждую строку кода
  4. Легко расширить — добавить новый метод 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_TOKEN
    • TELEGRAM_CHAT_ID
    • TELEGRAM_WEBHOOK_SECRET
  • Переменные прописаны в nuxt.config.ts
  • Создан server/utils/telegram.ts
  • Создан server/api/telegram/webhook.post.ts
  • Интегрированы вызовы в API эндпоинты
  • Зарегистрирован webhook после деплоя
  • Протестированы все типы уведомлений

Заключение

За несколько часов работы мы получили полноценную систему уведомлений:

  • Мгновенные алерты о всех важных событиях
  • Интерактивная модерация без захода в админку
  • Ноль зависимостей — только нативный fetch
  • Graceful degradation — если Telegram недоступен, приложение работает

Это решение легко расширить: добавить уведомления о других событиях, отправлять сообщения разным пользователям, интегрировать более сложные сценарии взаимодействия.

Telegram Bot API предоставляет множество возможностей — от простых сообщений до полноценных мини-приложений. Начните с простого и расширяйте по мере необходимости.

Комментарии

Войдите, чтобы оставить комментарий

Пока нет комментариев. Будьте первым!