discord.js v14 başvuru sistemi: kodla ya da kodsuz

Toscio ekibi · Güncellendi 22 Ekim 2026 · 5 dk okuma

discord.js v14 ile başvuru sistemi nasıl yapılır? Dört parçaya ihtiyacın var: Başvur butonlu bir panel, bir modal (Discord'un açılır formu; pencere başına en fazla 5 alan, yani uzun formlar birkaç pencereyi art arda bağlamalı), bir yetkili kanalına Kabul et ve Reddet butonlarıyla gönderilen bir kart ve kabulde member.roles.add artı başvurana bir DM. Bu rehber sana kısa ve çalışan bir iskelet, etrafına hâlâ kendin kurman gerekenlerin listesini ve bir bot bakımıyla uğraşmak istemiyorsan kodsuz yolu veriyor.

Kodlaman gerekenler

Parça discord.js v14 API'si
Başvur butonlu panel ButtonBuilder, ActionRowBuilder, channel.send
Form ModalBuilder, TextInputBuilder, interaction.showModal
Cevapları okumak interaction.isModalSubmit(), interaction.fields.getTextInputValue
Butonlu yetkili kartı EmbedBuilder, customId içinde başvuranın kimliğiyle ButtonBuilder
Karar member.roles.add, member.send, interaction.update

Minimal bir discord.js v14 iskeleti

Tek dosya, npm install discord.js, Node 18 ya da üstü. Üç kimliği doldur ve DISCORD_TOKEN değişkenini ayarla. Botun sadece Guilds intent'ine ihtiyacı var. Butonların ve mesajların metinleri İngilizce; kendi sunucuna göre değiştir.

// 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'; // Başvur butonunun kanalı
const STAFF_CHANNEL_ID = '000000000000000000'; // gizli yetkili kanalı
const ROLE_ID = '000000000000000000'; // kabulde verilen rol

const client = new Client({ intents: [GatewayIntentBits.Guilds] });

// Paneli açılışta yayınlar: bir kez çalıştır, sonra kaldır
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. Başvur butonu formu açar (modal başına en fazla 5 alan)
  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. Form gönderildi: yetkili kanalına bir kart yayınla
  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. Yetkili kartında Kabul et ya da Reddet
  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); // botun rolü ROLE_ID'nin üstünde olmalı
      } catch {
        await interaction.reply({ content: 'Could not give the role: check the role order.', flags: MessageFlags.Ephemeral });
        return;
      }
    }
    // Başvuran DM'lerini kapatmış olabilir: hatayı yok say
    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);

Başvuranın kimliği butonun customId alanında taşınır; böylece karar yeniden başlatmadan sonra da çalışır. Geri kalan her şey sadece yetkili mesajında durur.

Hâlâ kendin kurman gerekenler

İskelet çalışıyor ama gerçek bir başvuru sistemi daha fazlasını ister:

  • Depolama. Başvuruları sadece Discord mesajlarında değil, bir veritabanında tut; aramak, dışa aktarmak ve silinen bir mesajdan sonra da saklamak için.
  • Üye başına tek bekleyen başvuru, kimse aynı formu on kez göndermesin diye.
  • Ret sonrası bekleme süresi, üye ve form başına kaydedilmiş.
  • 5'ten fazla soru. Modal gönderimi başka bir modal açamaz: sıradaki pencereyi açan bir Devam et butonuyla yanıt ver ve adımlar arasında cevapları kaydet.
  • Yetkili izinleri. Burada sadece Sunucuyu Yönet izni olanlar karar verebiliyor; sen form başına yetkili rolleri isteyeceksin.
  • Rol sırası. Discord, botun kendi rolünden yüksekteki bir rol için roles.add isteğini reddeder: bunu algıla ve yetkililere neyi düzelteceklerini söyle.
  • Kapalı DM'ler. DM gönderilemediğinde yetkililere haber ver, başvurana başka bir yoldan ulaşsınlar.
  • Barındırma. 7/24 çalışan, çökünce yeniden başlayan ve discord.js değiştiğinde güncellenen bir süreç.

Toscio'nun kodsuz yaptıkları

Bunların hepsinin bakımıyla uğraşmak istemiyorsan kodsuz rehberimiz aynı akışı Toscio ile gösteriyor: /toscio setup, bir şablon ve panel yayında. Yukarıdaki listedeki her şey zaten hallediliyor: beşer soruluk adımlarla art arda bağlanan Discord formları, üye başına tek bekleyen başvuru, bekleme süresi, yetkili rolleri, rol sırası kontrolleri ve kapalı DM'lerin kartta belirtilmesi.

Üstüne her kartta yetkili notları ve başvuru geçmişi gelir. Ücretsiz plan; 10 soruluk 2 formla birlikte puanlı testi, kabulden önceki aşamayı (mülakat, deneme süresi) ve kontrol panelinde yetkili erişimini içerir.

Scriptini tut, bir webhook bağla (Premium)

Başvurulara ihtiyaç duyan bir scriptin, bir tablon ya da bir CAD/MDT'n zaten var mı? Onu tut, olayları Toscio göndersin. Form düzenleyicide gelişmiş ayarlardaki "Webhook (entegrasyonlar)" bölümünü aç ve bir https adresi gir. Toscio şunları gönderir:

Olay Ne zaman
application.submitted bir başvuru alındığında
application.staged kabulden önceki aşamaya geçtiğinde
application.accepted kabul edildiğinde
application.refused reddedildiğinde
ping "Test gönder"e tıkladığında (application değeri null)

Gövde JSON'dur:

{
  "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" }]
  }
}

Her istek iki başlık taşır: X-Toscio-Timestamp (saniye cinsinden Unix zamanı) ve X-Toscio-Signature; bu da sha256= ve ardından formun "İmza anahtarı" ile hesaplanan ${timestamp}.${ham gövde} değerinin onaltılık HMAC-SHA256'sıdır. Gövdeye güvenmeden önce bunu kontrol et:

// npm install express
const express = require('express');
const crypto = require('node:crypto');

const SECRET = process.env.TOSCIO_SIGNING_SECRET; // form düzenleyicideki "İmza anahtarı"
const app = express();

// Ham gövde: imza, alınan baytların tam haliyle hesaplanır
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 dakikadan eski istekleri reddet
  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); // 443 portunda https sunan bir ters proxy'nin arkasında

Bilmen gereken sınırlar:

  • Sadece herkese açık https adresleri: 443 portunda bir alan adı; IP adresi ya da yerel ad yok.
  • Yönlendirmeler izlenmez ve Toscio bir yanıt için en fazla 5 saniye bekler.
  • Yeniden deneme yok: başarısız bir istek tekrar gönderilmez.
  • Sunucuyu Yönet izni olmayan yetkililerden anahtar gizlenir.

Webhook'lar; yetkili oylaması, web formu ve ban itirazları gibi Premium özelliğidir (sunucu başına aylık 4,99 € ya da yıllık 49,90 €).

Sorular

discord.js v14 ile başvuru sistemi nasıl yapılır?

Butonlu bir mesaj gönder, butona tıklanınca bir modal aç (TextInputBuilder alanlarıyla bir ModalBuilder), modal gönderildiğinde cevapları oku, Kabul et ve Reddet butonlarıyla bir yetkili kanalına yayınla, sonra member.roles.add ile rolü ver ve başvurana DM at. Bu sayfadaki script tam olarak bunu yaklaşık 70 satırda yapıyor.

Bir Discord modalında 5'ten fazla soru olabilir mi?

Hayır. Bir modal en fazla 5 alan alır. Daha uzun formlar için birkaç modalı art arda bağlarsın; modal gönderimi doğrudan başka bir modal açamadığı için, arada cevapları kaydedip sıradakini açan bir Devam et butonuyla yanıt verirsin.

Toscio webhook imzası nasıl doğrulanır?

X-Toscio-Timestamp başlığı, bir nokta ve ham istek gövdesinin HMAC-SHA256 değerini formunun imza anahtarıyla, onaltılık olarak hesapla. Başına sha256= ekle ve X-Toscio-Signature ile sabit zamanlı bir karşılaştırmayla kıyasla. Zaman damgası çok eski olan istekleri de reddet.

Toscio başarısız webhook'ları yeniden dener mi?

Hayır. Her olay bir kez, 5 saniyelik zaman aşımıyla gönderilir. Adresin kapalıysa ya da çok yavaşsa o olay kaybolur; başvurunun kendisi normal şekilde işlenir. Eksikleri kapatmak için kontrol panelini ya da CSV dışa aktarmayı kullan.

Açık kaynak bir Discord başvuru botu var mı?

Evet, GitHub'da kendin barındırıp bakımını yaptığın birkaç tane bulursun. Toscio ise barındırılan bir bot: kurulacak bir şey yok; yine de kendi kodunu sürece katmak istersen webhook'ları her başvuru olayını scriptine gönderir.

Sıradaki başvurun iki dakika uzağında.

Toscio’yu ekle, /toscio setup yaz, bir şablon seç. Bu kadar.