~/wiki / telegram-boty / telegram-bot-moderation-queue

Как сделать модерацию заявок в Telegram-боте: очередь, статусы, кнопки

Основной чат

Чат для вайбкодеров: новости, гайды, поиск исполнителей, маркетплейс и разбор реальных кейсов.

$ cd раздел/ $ join vibe dev
Как сделать модерацию заявок в Telegram-боте: очередь, статусы, кнопки - обложка

Один из самых частых заказов на фрилансе — бот, который принимает заявки от пользователей и отправляет их администратору на проверку. Курс, услуга, публикация в каталоге, вступление в сообщество — везде нужна одна и та же механика: пришла заявка, модератор посмотрел, нажал «принять» или «отклонить», пользователь получил ответ.

Эта задача идеально решается через вайбкодинг: архитектура стандартная, Claude Code или Cursor знают её отлично, остаётся только грамотно поставить задачу и проверить результат.

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


Как работает система: схема целиком

Прежде чем писать код — нужно понять поток данных. Вот что происходит от момента заявки до публикации:

plaintext
Пользователь                   Бот                    Администратор
     │                          │                           │
     │  /start → заполнил форму │                           │
     │ ────────────────────────►│                           │
     │                          │  Сохранил в БД            │
     │                          │  status = 'pending'       │
     │                          │                           │
     │                          │  Переслал заявку ─────────►│
     │                          │  + кнопки [✅ Принять]    │
     │                          │           [❌ Отклонить]  │
     │                          │           [✏️ Правки]     │
     │                          │                           │
     │                          │          Нажал [✅ Принять]│
     │                          │◄──────────────────────────│
     │                          │                           │
     │                          │  status = 'approved'      │
     │                          │  Выполнить действие        │
     │  «Ваша заявка одобрена!» │  (публикация, доступ...)  │
     │◄─────────────────────────│                           │

Три участника, три зоны ответственности:

  • Пользователь — заполняет форму и ждёт ответа
  • База данных — хранит заявки и их статусы
  • Администратор — видит заявки, принимает решения кнопками

Статусы заявок

Это фундамент системы. Каждая заявка всегда находится ровно в одном статусе:

Статус Значение Что происходит
pending Ожидает проверки Заявка пришла, лежит в очереди
approved Одобрена Выполнено действие, пользователь уведомлён
rejected Отклонена Пользователь получил отказ с причиной
revision На доработке Пользователя попросили изменить заявку

Простое правило: никогда не удаляйте заявки из БД. Только меняйте статус. Это даёт полную историю и возможность вернуться к любой заявке.


База данных: минимальная структура

sql
CREATE TABLE applications (
    id            INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id       BIGINT NOT NULL,        -- Telegram ID пользователя
    username      TEXT,                   -- @никнейм (если есть)
    first_name    TEXT,                   -- Имя
    
    -- Данные заявки (адаптируйте под свой кейс)
    title         TEXT NOT NULL,          -- Название / тема
    description   TEXT,                   -- Описание
    contact       TEXT,                   -- Контакт (сайт, email...)
    
    -- Система модерации
    status        TEXT DEFAULT 'pending', -- pending / approved / rejected / revision
    admin_comment TEXT,                   -- Комментарий администратора
    
    -- Telegram-специфика
    admin_message_id INTEGER,             -- ID сообщения у администратора (для редактирования)
    
    -- Временные метки
    created_at    TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at    TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

admin_message_id — важное поле. Когда модератор нажимает кнопку, нужно обновить сообщение в его чате (убрать кнопки, показать статус). Для этого нужно знать ID того сообщения.


Реализация на Python (aiogram 3)

Aiogram — самый популярный фреймворк для Telegram-ботов на Python. Его хорошо знают Claude Code и Cursor, поэтому вайбкодить на нём проще всего.

Конфигурация

python
# config.py
import os

BOT_TOKEN = os.environ['BOT_TOKEN']
ADMIN_CHAT_ID = int(os.environ['ADMIN_CHAT_ID'])  # ID чата или группы администраторов

# Для группы модераторов — используйте отрицательный ID группы
# Для одного администратора — его личный Telegram ID

Инициализация

python
# bot.py
import asyncio
import sqlite3
from aiogram import Bot, Dispatcher, F
from aiogram.types import (
    Message, CallbackQuery,
    InlineKeyboardMarkup, InlineKeyboardButton
)
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from config import BOT_TOKEN, ADMIN_CHAT_ID

bot = Bot(token=BOT_TOKEN)
dp = Dispatcher()

# Инициализация БД
def init_db():
    conn = sqlite3.connect('applications.db')
    conn.execute('''
        CREATE TABLE IF NOT EXISTS applications (
            id               INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id          BIGINT NOT NULL,
            username         TEXT,
            first_name       TEXT,
            title            TEXT NOT NULL,
            description      TEXT,
            contact          TEXT,
            status           TEXT DEFAULT 'pending',
            admin_comment    TEXT,
            admin_message_id INTEGER,
            created_at       TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            updated_at       TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    ''')
    conn.commit()
    conn.close()

FSM: пошаговый сбор заявки

FSM (Finite State Machine) — механизм для пошаговых диалогов. Пользователь проходит шаги один за другим:

python
# states.py
from aiogram.fsm.state import State, StatesGroup

class ApplicationForm(StatesGroup):
    waiting_title       = State()  # Шаг 1: название
    waiting_description = State()  # Шаг 2: описание
    waiting_contact     = State()  # Шаг 3: контакт
    waiting_confirm     = State()  # Шаг 4: подтверждение

Обработчики пользовательской части

python
# handlers/user.py

@dp.message(Command('start'))
async def cmd_start(message: Message):
    await message.answer(
        "👋 Привет! Это бот для подачи заявок.\n\n"
        "Нажмите /apply чтобы подать заявку."
    )

@dp.message(Command('apply'))
async def cmd_apply(message: Message, state: FSMContext):
    await state.set_state(ApplicationForm.waiting_title)
    await message.answer(
        "📝 Начнём заполнение заявки.\n\n"
        "<b>Шаг 1/3.</b> Введите название вашего проекта:"
    )

@dp.message(ApplicationForm.waiting_title)
async def process_title(message: Message, state: FSMContext):
    if len(message.text) < 3:
        await message.answer("Название слишком короткое. Попробуйте ещё раз:")
        return
    
    await state.update_data(title=message.text)
    await state.set_state(ApplicationForm.waiting_description)
    await message.answer(
        "<b>Шаг 2/3.</b> Опишите проект подробнее:\n"
        "(что делает, для кого, на каком этапе)"
    )

@dp.message(ApplicationForm.waiting_description)
async def process_description(message: Message, state: FSMContext):
    await state.update_data(description=message.text)
    await state.set_state(ApplicationForm.waiting_contact)
    await message.answer(
        "<b>Шаг 3/3.</b> Оставьте контакт для связи:\n"
        "(сайт, email, @никнейм, номер телефона)"
    )

@dp.message(ApplicationForm.waiting_contact)
async def process_contact(message: Message, state: FSMContext):
    await state.update_data(contact=message.text)
    data = await state.get_data()
    
    # Показываем итог и просим подтвердить
    await state.set_state(ApplicationForm.waiting_confirm)
    
    confirm_kb = InlineKeyboardMarkup(inline_keyboard=[
        [
            InlineKeyboardButton(text="✅ Отправить", callback_data="submit_confirm"),
            InlineKeyboardButton(text="🔄 Начать заново", callback_data="submit_cancel"),
        ]
    ])
    
    await message.answer(
        f"📋 <b>Проверьте заявку:</b>\n\n"
        f"<b>Название:</b> {data['title']}\n"
        f"<b>Описание:</b> {data['description']}\n"
        f"<b>Контакт:</b> {data['contact']}\n\n"
        "Всё верно?",
        reply_markup=confirm_kb
    )

@dp.callback_query(F.data == "submit_cancel")
async def cancel_submission(callback: CallbackQuery, state: FSMContext):
    await state.clear()
    await callback.message.edit_text("Заявка отменена. Нажмите /apply чтобы начать заново.")
    await callback.answer()

@dp.callback_query(F.data == "submit_confirm")
async def confirm_submission(callback: CallbackQuery, state: FSMContext):
    data = await state.get_data()
    await state.clear()
    
    user = callback.from_user
    
    # Сохраняем в БД
    conn = sqlite3.connect('applications.db')
    cursor = conn.execute(
        '''INSERT INTO applications (user_id, username, first_name, title, description, contact)
           VALUES (?, ?, ?, ?, ?, ?)''',
        (user.id, user.username, user.first_name,
         data['title'], data['description'], data['contact'])
    )
    app_id = cursor.lastrowid
    conn.commit()
    conn.close()
    
    # Отправляем администратору
    await send_to_admin(app_id, user, data)
    
    # Подтверждаем пользователю
    await callback.message.edit_text(
        f"✅ <b>Заявка #{app_id} отправлена!</b>\n\n"
        "Мы рассмотрим её в течение 24 часов и сообщим результат."
    )
    await callback.answer("Заявка отправлена!")

Отправка заявки администратору с кнопками

python
async def send_to_admin(app_id: int, user, data: dict):
    """Отправляет заявку в чат администратора с кнопками модерации."""
    
    username_str = f"@{user.username}" if user.username else "нет никнейма"
    
    text = (
        f"🆕 <b>Новая заявка #{app_id}</b>\n\n"
        f"👤 <b>От:</b> {user.first_name} ({username_str})\n"
        f"🔗 <b>Ссылка:</b> tg://user?id={user.id}\n\n"
        f"📌 <b>Название:</b>\n{data['title']}\n\n"
        f"📝 <b>Описание:</b>\n{data['description']}\n\n"
        f"📬 <b>Контакт:</b>\n{data['contact']}\n\n"
        f"⏰ Статус: <i>ожидает проверки</i>"
    )
    
    # Кнопки для модератора
    admin_kb = InlineKeyboardMarkup(inline_keyboard=[
        [
            InlineKeyboardButton(text="✅ Принять", callback_data=f"admin_approve:{app_id}"),
            InlineKeyboardButton(text="❌ Отклонить", callback_data=f"admin_reject:{app_id}"),
        ],
        [
            InlineKeyboardButton(text="✏️ Запросить правки", callback_data=f"admin_revision:{app_id}"),
        ]
    ])
    
    msg = await bot.send_message(ADMIN_CHAT_ID, text, reply_markup=admin_kb)
    
    # Сохраняем ID сообщения у администратора — понадобится для обновления
    conn = sqlite3.connect('applications.db')
    conn.execute(
        'UPDATE applications SET admin_message_id = ? WHERE id = ?',
        (msg.message_id, app_id)
    )
    conn.commit()
    conn.close()

Обработчики кнопок администратора

python
# handlers/admin.py

@dp.callback_query(F.data.startswith("admin_approve:"))
async def admin_approve(callback: CallbackQuery):
    app_id = int(callback.data.split(":")[1])
    
    # Обновляем статус в БД
    conn = sqlite3.connect('applications.db')
    app = conn.execute(
        'SELECT user_id, title FROM applications WHERE id = ?', (app_id,)
    ).fetchone()
    
    conn.execute(
        'UPDATE applications SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
        ('approved', app_id)
    )
    conn.commit()
    conn.close()
    
    if not app:
        await callback.answer("Заявка не найдена!", show_alert=True)
        return
    
    user_id, title = app
    
    # Обновляем сообщение у администратора (убираем кнопки, добавляем статус)
    await callback.message.edit_text(
        callback.message.text + f"\n\n✅ <b>ОДОБРЕНО</b> — {callback.from_user.first_name}",
        reply_markup=None
    )
    
    # Уведомляем пользователя
    await bot.send_message(
        user_id,
        f"🎉 <b>Ваша заявка одобрена!</b>\n\n"
        f"<b>«{title}»</b> прошла модерацию и будет опубликована.\n\n"
        "Спасибо за участие!"
    )
    
    # TODO: здесь ваша бизнес-логика после одобрения
    # Например: добавить в каталог, выдать доступ, опубликовать...
    await publish_application(app_id)
    
    await callback.answer("Заявка одобрена!")


@dp.callback_query(F.data.startswith("admin_reject:"))
async def admin_reject(callback: CallbackQuery, state: FSMContext):
    app_id = int(callback.data.split(":")[1])
    
    # Просим ввести причину отказа
    await state.update_data(rejecting_app_id=app_id, admin_msg_id=callback.message.message_id)
    await state.set_state(AdminStates.waiting_reject_reason)
    
    await callback.message.answer(
        f"❌ Отклонение заявки #{app_id}\n\n"
        "Введите причину отказа (будет отправлена пользователю):"
    )
    await callback.answer()


@dp.callback_query(F.data.startswith("admin_revision:"))
async def admin_revision(callback: CallbackQuery, state: FSMContext):
    app_id = int(callback.data.split(":")[1])
    
    await state.update_data(revision_app_id=app_id)
    await state.set_state(AdminStates.waiting_revision_comment)
    
    await callback.message.answer(
        f"✏️ Запрос правок для заявки #{app_id}\n\n"
        "Что нужно исправить? (сообщение будет отправлено автору заявки):"
    )
    await callback.answer()


# FSM для ввода причины отказа
class AdminStates(StatesGroup):
    waiting_reject_reason    = State()
    waiting_revision_comment = State()


@dp.message(AdminStates.waiting_reject_reason)
async def process_reject_reason(message: Message, state: FSMContext):
    data = await state.get_data()
    app_id = data['rejecting_app_id']
    reason = message.text
    await state.clear()
    
    conn = sqlite3.connect('applications.db')
    app = conn.execute(
        'SELECT user_id, title, admin_message_id FROM applications WHERE id = ?', (app_id,)
    ).fetchone()
    
    conn.execute(
        '''UPDATE applications 
           SET status = 'rejected', admin_comment = ?, updated_at = CURRENT_TIMESTAMP
           WHERE id = ?''',
        (reason, app_id)
    )
    conn.commit()
    conn.close()
    
    user_id, title, admin_msg_id = app
    
    # Обновляем сообщение у администратора
    await bot.edit_message_text(
        chat_id=ADMIN_CHAT_ID,
        message_id=admin_msg_id,
        text=f"❌ Заявка #{app_id} отклонена\n\nПричина: {reason}",
        reply_markup=None
    )
    
    # Уведомляем пользователя
    await bot.send_message(
        user_id,
        f"😔 <b>Заявка не прошла модерацию</b>\n\n"
        f"<b>«{title}»</b>\n\n"
        f"<b>Причина:</b> {reason}\n\n"
        "Вы можете подать новую заявку через /apply"
    )
    
    await message.answer(f"✅ Заявка #{app_id} отклонена, пользователь уведомлён.")


@dp.message(AdminStates.waiting_revision_comment)
async def process_revision_comment(message: Message, state: FSMContext):
    data = await state.get_data()
    app_id = data['revision_app_id']
    comment = message.text
    await state.clear()
    
    conn = sqlite3.connect('applications.db')
    app = conn.execute(
        'SELECT user_id, title, admin_message_id FROM applications WHERE id = ?', (app_id,)
    ).fetchone()
    
    conn.execute(
        '''UPDATE applications
           SET status = 'revision', admin_comment = ?, updated_at = CURRENT_TIMESTAMP
           WHERE id = ?''',
        (comment, app_id)
    )
    conn.commit()
    conn.close()
    
    user_id, title, admin_msg_id = app
    
    await bot.edit_message_text(
        chat_id=ADMIN_CHAT_ID,
        message_id=admin_msg_id,
        text=f"✏️ Заявка #{app_id} отправлена на доработку\n\nКомментарий: {comment}",
        reply_markup=None
    )
    
    await bot.send_message(
        user_id,
        f"✏️ <b>Нужны правки в заявке</b>\n\n"
        f"<b>«{title}»</b>\n\n"
        f"<b>Комментарий:</b> {comment}\n\n"
        "Подайте исправленную заявку через /apply"
    )
    
    await message.answer(f"✅ Правки запрошены, пользователь уведомлён.")

Просмотр очереди администратором

Команда /queue показывает все заявки в ожидании:

python
@dp.message(Command('queue'))
async def cmd_queue(message: Message):
    # Только для администратора
    if message.chat.id != ADMIN_CHAT_ID and message.from_user.id != ADMIN_CHAT_ID:
        return
    
    conn = sqlite3.connect('applications.db')
    pending = conn.execute(
        '''SELECT id, first_name, username, title, created_at
           FROM applications
           WHERE status = 'pending'
           ORDER BY created_at ASC''',
    ).fetchall()
    conn.close()
    
    if not pending:
        await message.answer("✅ Очередь пуста, все заявки обработаны.")
        return
    
    text = f"📋 <b>Заявок в очереди: {len(pending)}</b>\n\n"
    
    for app in pending:
        app_id, first_name, username, title, created_at = app
        username_str = f"@{username}" if username else first_name
        # Форматируем дату
        date_str = created_at[:16].replace('T', ' ')
        text += f"#{app_id} · {username_str} · <i>{date_str}</i>\n{title}\n\n"
    
    await message.answer(text)

Команда /stats — статистика по статусам:

python
@dp.message(Command('stats'))
async def cmd_stats(message: Message):
    if message.chat.id != ADMIN_CHAT_ID and message.from_user.id != ADMIN_CHAT_ID:
        return
    
    conn = sqlite3.connect('applications.db')
    stats = conn.execute(
        '''SELECT status, COUNT(*) as cnt
           FROM applications
           GROUP BY status'''
    ).fetchall()
    conn.close()
    
    labels = {
        'pending': '⏳ Ожидают',
        'approved': '✅ Одобрено',
        'rejected': '❌ Отклонено',
        'revision': '✏️ На доработке',
    }
    
    total = sum(row[1] for row in stats)
    text = f"📊 <b>Статистика заявок (всего: {total})</b>\n\n"
    
    for status, count in stats:
        label = labels.get(status, status)
        text += f"{label}: {count}\n"
    
    await message.answer(text)

Вариант на Node.js (Grammy)

Если вы вайбкодите на JavaScript — вот аналогичная архитектура на Grammy:

javascript
// bot.js
import { Bot, InlineKeyboard, session } from 'grammy';
import { conversations, createConversation } from '@grammyjs/conversations';
import Database from 'better-sqlite3';

const bot = new Bot(process.env.BOT_TOKEN);
const ADMIN_CHAT_ID = Number(process.env.ADMIN_CHAT_ID);
const db = new Database('applications.db');

// Инициализация БД
db.exec(`
  CREATE TABLE IF NOT EXISTS applications (
    id               INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id          INTEGER NOT NULL,
    username         TEXT,
    first_name       TEXT,
    title            TEXT NOT NULL,
    description      TEXT,
    contact          TEXT,
    status           TEXT DEFAULT 'pending',
    admin_comment    TEXT,
    admin_message_id INTEGER,
    created_at       TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  )
`);

// Conversation: пошаговый сбор заявки
async function applyConversation(conversation, ctx) {
  await ctx.reply('📝 <b>Шаг 1/3.</b> Введите название проекта:');
  const titleCtx = await conversation.wait();
  const title = titleCtx.message?.text;
  
  await ctx.reply('📝 <b>Шаг 2/3.</b> Опишите проект:');
  const descCtx = await conversation.wait();
  const description = descCtx.message?.text;
  
  await ctx.reply('📝 <b>Шаг 3/3.</b> Контакт для связи:');
  const contactCtx = await conversation.wait();
  const contact = contactCtx.message?.text;
  
  // Подтверждение
  const keyboard = new InlineKeyboard()
    .text('✅ Отправить', 'submit_confirm')
    .text('🔄 Начать заново', 'submit_cancel');
  
  await ctx.reply(
    `📋 <b>Проверьте заявку:</b>\n\n`+
    `<b>Название:</b> ${title}\n`+
    `<b>Описание:</b> ${description}\n`+
    `<b>Контакт:</b> ${contact}`,
    { reply_markup: keyboard }
  );
  
  const confirmCtx = await conversation.waitForCallbackQuery(['submit_confirm', 'submit_cancel']);
  
  if (confirmCtx.callbackQuery.data === 'submit_cancel') {
    await confirmCtx.editMessageText('Заявка отменена. Нажмите /apply чтобы начать заново.');
    return;
  }
  
  // Сохраняем в БД
  const user = ctx.from;
  const result = db.prepare(
    `INSERT INTO applications (user_id, username, first_name, title, description, contact)
     VALUES (?, ?, ?, ?, ?, ?)`
  ).run(user.id, user.username, user.first_name, title, description, contact);
  
  const appId = result.lastInsertRowid;
  
  // Отправляем администратору
  const adminKeyboard = new InlineKeyboard()
    .text('✅ Принять', `admin_approve:${appId}`)
    .text('❌ Отклонить', `admin_reject:${appId}`)
    .row()
    .text('✏️ Запросить правки', `admin_revision:${appId}`);
  
  const adminMsg = await bot.api.sendMessage(
    ADMIN_CHAT_ID,
    `🆕 <b>Новая заявка #${appId}</b>\n\n`+
    `👤 <b>От:</b> ${user.first_name} ${user.username ? '@'+user.username : ''}\n\n`+
    `📌 <b>Название:</b> ${title}\n\n`+
    `📝 <b>Описание:</b> ${description}\n\n`+
    `📬 <b>Контакт:</b> ${contact}`,
    { reply_markup: adminKeyboard }
  );
  
  // Сохраняем ID сообщения администратора
  db.prepare('UPDATE applications SET admin_message_id = ? WHERE id = ?')
    .run(adminMsg.message_id, appId);
  
  await confirmCtx.editMessageText(
    `✅ <b>Заявка #${appId} отправлена!</b>\n\nОжидайте ответа в течение 24 часов.`
  );
}

// Подключаем conversations
bot.use(session({ initial: () => ({}) }));
bot.use(conversations());
bot.use(createConversation(applyConversation, 'apply'));

// Команды
bot.command('apply', ctx => ctx.conversation.enter('apply'));

// Обработчики кнопок администратора
bot.callbackQuery(/^admin_approve:(\d+)$/, async ctx => {
  const appId = parseInt(ctx.match[1]);
  const app = db.prepare('SELECT * FROM applications WHERE id = ?').get(appId);
  
  db.prepare(`UPDATE applications SET status = 'approved' WHERE id = ?`).run(appId);
  
  await ctx.editMessageText(
    ctx.callbackQuery.message.text + `\n\n✅ ОДОБРЕНО — ${ctx.from.first_name}`,
    { reply_markup: undefined }
  );
  
  await bot.api.sendMessage(
    app.user_id,
    `🎉 <b>Ваша заявка одобрена!</b>\n\n«${app.title}» прошла модерацию.`
  );
  
  await ctx.answerCallbackQuery('Заявка одобрена!');
});

bot.start();

Нюансы которые часто забывают

Проверка прав администратора

Любой может отправить боту сообщение с текстом /queue или даже попытаться сконструировать нужный callback. Всегда проверяйте:

python
def is_admin(user_id: int) -> bool:
    return user_id == ADMIN_CHAT_ID
    # Или: return user_id in ADMIN_IDS (список нескольких)

@dp.callback_query(F.data.startswith("admin_"))
async def admin_action(callback: CallbackQuery):
    if not is_admin(callback.from_user.id):
        await callback.answer("⛔ Нет прав", show_alert=True)
        return
    # ... остальная логика

Лимит кнопок и длина callback_data

Telegram ограничивает callback_data — максимум 64 байта. admin_approve:12345 — это 20 байт, всё в порядке. Но не кладите туда длинные строки.

Один ряд (row) — максимум 8 кнопок. Больше трёх кнопок в ряд — уже нечитаемо. Оптимально: 2-3 кнопки в первом ряду, дополнительные — отдельным рядом.

Что делать если пользователь заблокировал бота

При отправке уведомления пользователю может прийти ошибка 403 Forbidden — это значит, пользователь заблокировал бота. Обрабатывайте явно:

python
try:
    await bot.send_message(user_id, text)
except TelegramForbiddenError:
    # Пользователь заблокировал бота — просто логируем
    print(f"User {user_id} blocked the bot")
except TelegramBadRequest as e:
    print(f"Bad request: {e}")

Несколько администраторов

Если модераторов несколько — используйте группу Telegram:

python
# ADMIN_CHAT_ID = -100123456789 (отрицательный ID группы)

# Проверка: пользователь является членом группы
async def is_admin(user_id: int) -> bool:
    try:
        member = await bot.get_chat_member(ADMIN_CHAT_ID, user_id)
        return member.status in ('administrator', 'creator', 'member')
    except:
        return False

Кнопку нажать сможет любой участник группы — это удобно для командной модерации.


Публикация после одобрения

После status = 'approved' запускается ваша бизнес-логика. Что это может быть:

Добавление в каталог:

python
async def publish_application(app_id: int):
    app = get_application(app_id)
    
    # Публикуем в канал
    await bot.send_message(
        CHANNEL_ID,
        f"📌 <b>{app['title']}</b>\n\n"
        f"{app['description']}\n\n"
        f"📬 Контакт: {app['contact']}"
    )

Выдача доступа:

python
async def publish_application(app_id: int):
    app = get_application(app_id)
    
    # Создаём invite link в закрытую группу
    link = await bot.create_chat_invite_link(
        PRIVATE_GROUP_ID,
        member_limit=1,
        expire_date=datetime.now() + timedelta(days=1)
    )
    
    await bot.send_message(
        app['user_id'],
        f"🔗 Ваша персональная ссылка для вступления:\n{link.invite_link}\n\n"
        "Действует 24 часа."
    )

Запись в внешнюю систему:

python
async def publish_application(app_id: int):
    app = get_application(app_id)
    
    # Отправить в CRM, Notion, Airtable, Google Sheets...
    await send_to_crm(app)

Промты для вайбкодинга

Если хотите собрать это через Claude Code или Cursor — вот готовые задачи:

Базовая система с нуля:

plaintext
Создай Telegram-бота на Python (aiogram 3) с модерацией заявок.

Пользователь подаёт заявку через /apply — 3 шага: название, описание, контакт.
Заявка сохраняется в SQLite с полями: user_id, title, description, contact, status.
Бот пересылает заявку в чат ADMIN_CHAT_ID с кнопками: Принять / Отклонить / Правки.
При нажатии — статус обновляется, пользователь получает уведомление.
При отклонении или правках — бот просит ввести причину/комментарий.

Конфигурация через .env: BOT_TOKEN, ADMIN_CHAT_ID.

Добавить команды администратора:

plaintext
Добавь в существующего бота команды для администратора:
/queue — список всех заявок со статусом pending
/stats — количество заявок по статусам
/app {id} — показать конкретную заявку с кнопками управления

Проверяй что команды выполняет только пользователь с ADMIN_CHAT_ID.

Добавить публикацию в канал:

plaintext
После одобрения заявки добавь публикацию в Telegram-канал CHANNEL_ID.
Формат поста: название, описание, контакт.
После публикации — сохрани ID опубликованного сообщения в поле channel_message_id.
Добавь кнопку "Удалить из канала" в сообщение администратора после публикации.

Итог

Система модерации заявок — стандартный паттерн, который собирается из нескольких простых блоков: FSM для сбора данных, inline-кнопки для действий администратора, статусы в базе данных, уведомления пользователю.

Главные принципы:

  • Заявки никогда не удаляются — только меняют статус
  • Кнопки администратора несут app_id в callback_data
  • Права всегда проверяются на сервере, не только в интерфейсе
  • Обработка ошибок при уведомлении пользователя (мог заблокировать бота)

Это задача, которую Claude Code собирает за один хороший промт — попробуйте поставить задачу чётко, и получите рабочий прототип за минуты.

$ cd ../ ← назад к Telegram-боты