Система заявок на discord.js v14 (или без кода)
Как сделать систему заявок на discord.js v14? Нужны четыре части: панель с кнопкой Подать заявку, модальное окно (всплывающая форма Discord, максимум 5 полей в одном окне, так что длинные формы приходится собирать из нескольких окон), карточка в канале администрации с кнопками Одобрить и Отклонить и при одобрении — member.roles.add плюс ЛС кандидату. В этом гайде — короткий рабочий каркас, список того, что останется доделать вокруг него, и путь без кода, если поддерживать своего бота не хочется.
Что нужно написать
| Часть | API discord.js v14 |
|---|---|
| Панель с кнопкой «Подать заявку» | ButtonBuilder, ActionRowBuilder, channel.send |
| Форма | ModalBuilder, TextInputBuilder, interaction.showModal |
| Чтение ответов | interaction.isModalSubmit(), interaction.fields.getTextInputValue |
| Карточка для администрации с кнопками | EmbedBuilder, ButtonBuilder с ID кандидата в customId |
| Решение | member.roles.add, member.send, interaction.update |
Минимальный каркас на discord.js v14
Один файл, npm install discord.js, Node 18 или новее. Впиши три ID и задай DISCORD_TOKEN. Боту нужен только intent Guilds.
// discord.js v14: npm install discord.js
const {
Client, GatewayIntentBits, Events, ActionRowBuilder, ButtonBuilder, ButtonStyle,
ModalBuilder, TextInputBuilder, TextInputStyle, EmbedBuilder, MessageFlags, PermissionFlagsBits,
} = require('discord.js');
const APPLY_CHANNEL_ID = '000000000000000000'; // канал с кнопкой «Подать заявку»
const STAFF_CHANNEL_ID = '000000000000000000'; // приватный канал администрации
const ROLE_ID = '000000000000000000'; // роль, выдаваемая при одобрении
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
// Публикует панель при запуске: запусти один раз, потом убери
client.once(Events.ClientReady, async (c) => {
const channel = await c.channels.fetch(APPLY_CHANNEL_ID);
const button = new ButtonBuilder().setCustomId('apply').setLabel('Apply').setStyle(ButtonStyle.Primary);
await channel.send({ content: 'Want to join the staff? Click below.', components: [new ActionRowBuilder().addComponents(button)] });
});
client.on(Events.InteractionCreate, async (interaction) => {
// 1. Кнопка «Подать заявку» открывает форму (максимум 5 полей в окне)
if (interaction.isButton() && interaction.customId === 'apply') {
const age = new TextInputBuilder().setCustomId('age').setLabel('How old are you?')
.setStyle(TextInputStyle.Short).setMaxLength(3).setRequired(true);
const why = new TextInputBuilder().setCustomId('why').setLabel('Why do you want to join the staff?')
.setStyle(TextInputStyle.Paragraph).setMaxLength(1000).setRequired(true);
const modal = new ModalBuilder().setCustomId('apply-form').setTitle('Staff application')
.addComponents(new ActionRowBuilder().addComponents(age), new ActionRowBuilder().addComponents(why));
await interaction.showModal(modal);
return;
}
// 2. Форма отправлена: публикуем карточку в канале администрации
if (interaction.isModalSubmit() && interaction.customId === 'apply-form') {
const embed = new EmbedBuilder().setTitle(`Application from ${interaction.user.username}`).addFields(
{ name: 'Age', value: interaction.fields.getTextInputValue('age') },
{ name: 'Why', value: interaction.fields.getTextInputValue('why') },
);
const buttons = new ActionRowBuilder().addComponents(
new ButtonBuilder().setCustomId(`accept:${interaction.user.id}`).setLabel('Accept').setStyle(ButtonStyle.Success),
new ButtonBuilder().setCustomId(`decline:${interaction.user.id}`).setLabel('Decline').setStyle(ButtonStyle.Danger),
);
const staff = await interaction.client.channels.fetch(STAFF_CHANNEL_ID);
await staff.send({ embeds: [embed], components: [buttons] });
await interaction.reply({ content: 'Application sent!', flags: MessageFlags.Ephemeral });
return;
}
// 3. «Одобрить» или «Отклонить» на карточке администрации
if (interaction.isButton() && /^(accept|decline):\d+$/.test(interaction.customId)) {
if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) {
await interaction.reply({ content: 'Staff only.', flags: MessageFlags.Ephemeral });
return;
}
const [action, userId] = interaction.customId.split(':');
const member = await interaction.guild.members.fetch(userId).catch(() => null);
if (!member) {
await interaction.reply({ content: 'This member left the server.', flags: MessageFlags.Ephemeral });
return;
}
if (action === 'accept') {
try {
await member.roles.add(ROLE_ID); // роль бота должна быть выше ROLE_ID
} catch {
await interaction.reply({ content: 'Could not give the role: check the role order.', flags: MessageFlags.Ephemeral });
return;
}
}
// У кандидата могут быть закрыты ЛС: игнорируем ошибку
await member.send(action === 'accept' ? 'Your application was accepted!' : 'Your application was declined.').catch(() => {});
await interaction.update({ content: `${action === 'accept' ? '✅ Accepted' : '❌ Declined'} by ${interaction.user}`, components: [] });
}
});
client.login(process.env.DISCORD_TOKEN);
ID кандидата передаётся в customId кнопки, так что решение работает и после перезапуска. Всё остальное живёт только в сообщении для администрации.
Что останется сделать самому
Каркас работает, но настоящей системе заявок нужно больше:
- Хранение. Держи заявки в базе данных, а не только в сообщениях Discord, чтобы искать их, выгружать и не терять после удаления сообщения.
- Одна заявка на рассмотрении на участника, чтобы никто не отправлял одну форму десять раз.
- Пауза после отказа, отдельно для каждого участника и каждой формы.
- Больше 5 вопросов. Отправка модального окна не может открыть другое окно: ответь кнопкой Продолжить, которая откроет следующее, и сохраняй ответы между шагами.
- Права администрации. Здесь решать может только «Управлять сервером»; тебе понадобятся роли администрации для каждой формы.
- Порядок ролей. Discord отклоняет
roles.addдля роли выше роли бота: отлавливай это и подсказывай администрации, что исправить. - Закрытые ЛС. Сообщай администрации, если ЛС не удалось отправить, чтобы кандидата предупредили другим способом.
- Хостинг. Процесс, который работает 24/7, перезапускается после падения и обновляется, когда меняется discord.js.
Что Toscio делает без кода
Если поддерживать всё это не хочется, наш гайд без кода показывает тот же процесс в Toscio: /toscio setup, шаблон — и панель уже работает. Всё из списка выше уже сделано: родные формы Discord по пять вопросов за шаг, одна заявка на рассмотрении на участника, пауза, роли администрации, проверка порядка ролей и пометка о закрытых ЛС на карточке.
Сверху — заметки администрации и история кандидата на каждой карточке. В бесплатный план входят тест с оценкой, этап перед одобрением (собеседование, испытательный срок) и доступ администрации к дашборду, с 2 формами на 10 вопросов.
Оставь свой скрипт и подключи вебхук (Premium)
У тебя уже есть скрипт, таблица или CAD/MDT, которым нужны заявки? Оставь их, а события пусть отправляет Toscio. В редакторе формы открой «Вебхук (интеграции)» в дополнительных настройках и укажи адрес https. Toscio отправляет:
| Событие | Когда |
|---|---|
application.submitted |
заявка получена |
application.staged |
она переведена на этап перед одобрением |
application.accepted |
она одобрена |
application.refused |
она отклонена |
ping |
ты нажал «Отправить тест» (application равно null) |
Тело — JSON:
{
"event": "application.accepted",
"sentAt": "2026-10-22T18:04:11.000Z",
"guildId": "123456789012345678",
"form": { "id": 42, "name": "Staff recruitment" },
"application": {
"id": 1337,
"status": "accepted",
"userId": "234567890123456789",
"username": "alex",
"submittedAt": "2026-10-21T09:12:00.000Z",
"decidedAt": "2026-10-22T18:04:10.000Z",
"reviewerId": "345678901234567890",
"reason": null,
"stage": null,
"quiz": { "score": 3, "total": 4, "passed": true, "passPercent": 75 },
"web": false,
"answers": [{ "question": "How old are you?", "answer": "19" }]
}
}
В каждом запросе два заголовка: X-Toscio-Timestamp (время Unix в секундах) и X-Toscio-Signature — это sha256= и шестнадцатеричный HMAC-SHA256 от ${timestamp}.${сырое тело}, посчитанный «Секретом подписи» формы. Проверяй его, прежде чем доверять телу:
// npm install express
const express = require('express');
const crypto = require('node:crypto');
const SECRET = process.env.TOSCIO_SIGNING_SECRET; // «Секрет подписи» из редактора формы
const app = express();
// Сырое тело: подпись считается по точным полученным байтам
app.post('/toscio-webhook', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = req.get('X-Toscio-Timestamp') ?? '';
const signature = Buffer.from(req.get('X-Toscio-Signature') ?? '');
const expected = Buffer.from('sha256=' + crypto.createHmac('sha256', SECRET).update(`${timestamp}.`).update(req.body).digest('hex'));
const valid = signature.length === expected.length && crypto.timingSafeEqual(signature, expected);
const recent = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300; // отклоняем запросы старше 5 минут
if (!valid || !recent) return res.sendStatus(401);
const payload = JSON.parse(req.body.toString('utf8'));
if (payload.event === 'application.accepted') {
console.log(`${payload.application.username} accepted on ${payload.form.name}`);
}
res.sendStatus(204);
});
app.listen(3000); // за обратным прокси, который отдаёт https на порту 443
Что нужно знать об ограничениях:
- Только публичные адреса https: доменное имя на порту 443, без IP-адреса и локального имени.
- Перенаправления не выполняются, а ответа Toscio ждёт максимум 5 секунд.
- Без повторов: неудачный запрос не отправляется снова.
- Секрет скрыт от участников администрации без права «Управлять сервером».
Вебхуки входят в Premium (4,99 €/мес. или 49,90 €/год за сервер), как и голосование администрации, веб-форма и обжалование бана.
Вопросы
Как сделать систему заявок на discord.js v14?
Опубликуй сообщение с кнопкой, по клику открой модальное окно (ModalBuilder с полями TextInputBuilder), прочитай ответы при отправке окна, отправь их в канал администрации с кнопками «Одобрить» и «Отклонить», затем выдай роль через member.roles.add и напиши кандидату в ЛС. Скрипт на этой странице делает ровно это примерно в 70 строк.
Можно ли в модальном окне Discord больше 5 вопросов?
Нет. В модальном окне максимум 5 полей. Для длинных форм цепляют несколько окон подряд, а так как отправка окна не может сразу открыть другое окно, отвечают кнопкой «Продолжить», которая открывает следующее, и сохраняют ответы между шагами.
Как проверить подпись вебхука Toscio?
Посчитай HMAC-SHA256 от заголовка X-Toscio-Timestamp, точки и сырого тела запроса с «Секретом подписи» своей формы, в шестнадцатеричном виде. Добавь в начало sha256= и сравни с X-Toscio-Signature сравнением за постоянное время. Ещё отклоняй запросы со слишком старым временем.
Toscio повторяет неудачные вебхуки?
Нет. Каждое событие отправляется один раз, с таймаутом 5 секунд. Если твой адрес недоступен или отвечает слишком медленно, это событие теряется; сама заявка обрабатывается как обычно. Чтобы наверстать, используй дашборд или экспорт в CSV.
Есть ли бот для заявок в Discord с открытым кодом?
Да, на GitHub их несколько, но хостить и поддерживать их придётся самому. Toscio, наоборот, готовый бот на хостинге: ничего разворачивать не нужно, а если ты всё же хочешь подключить свой код, его вебхуки отправляют каждое событие заявки в твой скрипт.


