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

How to moderate applications in the Telegram bot: queue, statuses, buttons

Main chat

A chat for vibe coders: news, guides, live cases, marketplace, and finding executors.

$ cd section/ $ join vibe dev
How to moderate applications in the Telegram bot: queue, statuses, buttons - обложка

One of the most frequent freelance orders is a bot that accepts applications from users and sends them to the administrator for verification. Course, service, publication in the catalog, joining the community - everywhere you need the same mechanic: the application came, the moderator looked, clicked "accept" or "reject", the user received the answer.

This problem is ideally solved through vibcoding: the architecture is standard, Claude Code or Cursor know it perfectly, it remains only to correctly set the task and check the result.

In this article, we analyze the complete mechanics: how the queue is arranged, how statuses work, how to make buttons for the admin and how the whole system is held together.


How the system works: the whole scheme

Before you write code, you need to understand the flow of data. This is what happens from the time of application to publication:

plaintext
User Bot Administrator
Objective
← /start → Completed the Form
► ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ► ♥
Save it to the BD versus
versus status = 'pending' versus
Objective
I sent you an application ─ ─ ─ ─ ─ ─ ►
versus + buttons [  Adopt]
versus [  Reject]
← ← [Read More]
Objective
versus versus pressing [  Take]
♥ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ .
Objective
versus status = 'approved' versus
► Perform the action versus
“Your application is approved!” (Publishing, access...) ♥
← ― ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ Object

Three participants, three areas of responsibility:

  • **User ** - Fills out the form and waits for a response
  • Database - Stores applications and their statuses
  • Administrator - sees applications, makes decisions with buttons

Status of applications

It's the foundation of the system. Each application is always in exactly the same status:

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

A simple rule of thumb: Never delete applications from DB. Just change your status. This gives you a complete history and the opportunity to return to any application.


Database: minimum structure

sql
CREATE TABLE applications
id INTEGER PRIMARY KEY AUTOINCREMENT
user id BIGINT NOT NULL - Telegram user ID
username TEXT, -- @nickname (if any)
first name TEXT, -- Name

Application data (adapt to your case)
Title TEXT NOT NULL - Title / Topic
description TEXT - Description
Contact TEXT - Contact (website, email...)

- Moderation system.
status TEXT DEFAULT 'pending', - pending / approved / rejected / revision
admin comment TEXT - Administrator comment

- Telegram-specificity
admin message id INTEGER, - Message ID with the administrator (for editing)

- Time stamps.
created at TIMESTAMP DEFAULT CURRENT TIMESTAMP,
updated at TIMESTAMP DEFAULT CURRENT TIMESTAMP
);

admin_message_id is an important field. When the moderator presses the button, you need to update the message in his chat (remove the buttons, show the status). You need to know the ID of that message.


Python implementation (aiogram 3)

Aiogram is the most popular framework for Telegram bots in Python. It is well known Claude Code and Cursor, so it is the easiest to use it.

The configuration

python
#config. py
import

BOT TOKEN = os.environ['BOT TOKEN']
ADMIN CHAT ID = int(os.environ['ADMIN CHAT ID']) #ID of a chat or admin group

# For a group of moderators, use a negative group ID
# For one administrator - his personal Telegram ID

Initialization

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: step-by-step application collection

FSM (Finite State Machine) is a step-by-step dialogue mechanism. The user takes steps one by one:

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: подтверждение

Processors of the user part

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("Заявка отправлена!")

Sending an application to the administrator with buttons

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()

Administrator button handlers

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"✅ Правки запрошены, пользователь уведомлён.")

Viewing the queue by the administrator

The /queue team shows all applications waiting for:

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 team - Status statistics:

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)

Option on Node.js (Grammy)

If you use JavaScript, here is a similar architecture on the 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();

Nuances that are often forgotten

Verification of administrator rights

Anyone can send a message with /queue text to the bot, or even try to construct the right callback. Always check:

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
    # ... остальная логика

Button limit and callback data length

Telegram limits callback_data to a maximum of 64 bytes admin_approve:12345 is 20 bytes, all right. But don't put long lines in there.

One row (row) - maximum 8 buttons. More than three buttons in a row is already unreadable. Optimal: 2-3 buttons in the first row, additional ones in a separate row.

What to do if the user has blocked the bot

When sending a notification, the user may receive a 403 Forbidden error, which means that the user has blocked the bot. Process explicitly:

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}")

Several administrators

If there are several moderators, use the Telegram group:

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

Any member of the group can press the button - it is convenient for team moderation.


Publication after approval

After status = 'approved', your business logic starts. What could it be

Addition to catalogue:

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']}"
    )

** Issuance of access:**

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 часа."
    )

Recording to external system:

python
async def publish application(app id: int):
app = get application(app id)

# Send to CRM, Notion, Airtable, Google Sheets...
Wait to send to crm(app)

Vibcoding proms

If you want to collect it through Claude Code or Cursor, here are the finished tasks:

** Basic system from scratch:**

plaintext
Create a Telegram bot in Python (aiogram 3) with order moderation.

The user submits an application via /apply - 3 steps: name, description, contact.
The application is saved in SQLite with the following fields: user id, title, description, contact, status.
The bot sends the application to the ADMIN CHAT ID chat with buttons: Accept/Reject/Edit.
When you click the status is updated, the user receives a notification.
When rejected or edited, the bot asks for a reason/comment.

Configuration via .env: BOT TOKEN, ADMIN CHAT ID.

Add administrator commands:

plaintext
Add to the existing bot commands for the administrator:
/queue – list of all applications with pending status
/stats - number of applications by status n
/app {id} - show a specific application with control buttons

Check that the commands are executed only by the user with ADMIN CHAT ID.

Add publication to the channel:

plaintext
After approval of the application, add the publication in the Telegram channel CHANNEL ID.
Post format: name, description, contact.
After publication, save the ID of the posted message in the channel message id field.
Add the "Delete from the channel" button to the administrator's message after publication.

Outcome

The application moderation system is a standard pattern that is collected from several simple blocks: FSM for data collection, inline buttons for administrator actions, statuses in the database, notifications to the user.

Basic principles:

  • Applications are never deleted, they only change status
  • Administrator buttons carry app_id in callback_data
  • Rights are always checked on the server, not only in the interface
  • Error handling when notifying the user (could block the bot)

This is a task that Claude Code collects in one good propt — try to set the task clearly, and get a working prototype in minutes.

$ cd ../ ← back to Telegram bots