aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Discord Botları

Discord Çekiliş Botu: Süreli Çekiliş ve Kazanan Seçimi

Bir discord çekiliş botu, sunucundaki etkinlikleri otomatikleştiren en pratik araçlardandır: bir komutla çekilişi başlatırsın, üyeler tek tıkla katılır, süre dolunca bot adil biçimde kazananı seçip duyurur. Bu yazıda discord.js v14 ile süreli bir çekiliş sistemini sıfırdan kuracağız: butonla katılımcı toplama, geri sayım, rastgele kazanan seçimi ve botu yeniden başlatınca çekilişin kaybolmaması için kalıcılık. Reaksiyon (emoji) tabanlı eski yöntem yerine butonları kullanacağız; çünkü daha temiz, daha güvenilir ve manipülasyona daha kapanıktır.

Çekiliş sisteminin mimarisi

Bir çekiliş özünde üç durumdan oluşur: başlatma, katılım toplama ve sonlandırma. Her aktif çekiliş için şu bilgileri saklaman gerekir:

  • Çekilişin yapıldığı kanal ve mesaj kimliği (messageId).
  • Ödül metni ve kazanan sayısı (winnerCount).
  • Bitiş zamanı (endsAt, mutlaka mutlak bir zaman damgası).
  • Katılımcıların kullanıcı kimlikleri.

Bu durumu yalnızca bellekte (Map) tutarsan bot her yeniden başladığında tüm aktif çekilişler kaybolur. Bu yüzden kalıcı bir depo (SQLite gibi) şart; ama önce mantığı netleştirmek için akışı kuralım, sonra kalıcılığı ekleriz.

Çekilişi başlatan slash komutu

Çekilişi bir slash komutla başlatıyoruz. Süreyi 10m, 2h, 1d gibi okunabilir biçimde almak, kullanıcı deneyimini ciddi şekilde iyileştirir. Önce bu metni milisaniyeye çeviren küçük bir yardımcı yazalım:

function parseDuration(input) {
  const match = input.match(/^(\d+)\s*(s|m|h|d)$/i);
  if (!match) return null;
  const value = parseInt(match[1], 10);
  const unit = match[2].toLowerCase();
  const factor = { s: 1000, m: 60000, h: 3600000, d: 86400000 };
  return value * factor[unit];
}

Komutun kendisi ödül, süre ve kazanan sayısını alır; ardından bir embed ile birlikte "Katıl" butonunu gönderir:

const {
  SlashCommandBuilder,
  EmbedBuilder,
  ButtonBuilder,
  ButtonStyle,
  ActionRowBuilder,
} = require('discord.js');

const data = new SlashCommandBuilder()
  .setName('cekilis')
  .setDescription('Yeni bir çekiliş başlat')
  .addStringOption(o =>
    o.setName('odul').setDescription('Ödül').setRequired(true))
  .addStringOption(o =>
    o.setName('sure').setDescription('Örn: 10m, 2h, 1d').setRequired(true))
  .addIntegerOption(o =>
    o.setName('kazanan').setDescription('Kazanan sayısı').setMinValue(1));

async function execute(interaction) {
  const prize = interaction.options.getString('odul');
  const ms = parseDuration(interaction.options.getString('sure'));
  const winners = interaction.options.getInteger('kazanan') ?? 1;
  if (!ms) {
    return interaction.reply({ content: 'Geçersiz süre formatı.', ephemeral: true });
  }

  const endsAt = Date.now() + ms;
  const embed = new EmbedBuilder()
    .setTitle('🎉 Çekiliş')
    .setDescription(`**Ödül:** ${prize}\n**Kazanan:** ${winners}\n**Bitiş:** <t:${Math.floor(endsAt / 1000)}:R>`)
    .setColor(0x5865f2);

  const join = new ButtonBuilder()
    .setCustomId('giveaway_join')
    .setLabel('Katıl')
    .setEmoji('🎉')
    .setStyle(ButtonStyle.Primary);

  const row = new ActionRowBuilder().addComponents(join);
  const message = await interaction.reply({
    embeds: [embed], components: [row], fetchReply: true,
  });

  // Çekilişi kaydet (aşağıda kalıcılık bölümüne bakın)
  createGiveaway({ messageId: message.id, channelId: message.channelId,
    prize, winners, endsAt, participants: [] });
}

<t:...:R> biçimi, Discord'un yerleşik göreli zaman damgasıdır; "3 saat sonra" gibi her kullanıcının saat dilimine göre otomatik gösterilir. Sabit metin yazmaktan çok daha şıktır.

Butonla katılımcı toplama

"Katıl" butonuna her tıklandığında kullanıcıyı çekilişin katılımcı listesine ekliyoruz. Aynı kişinin iki kez sayılmaması için kullanıcı kimliklerini bir küme (Set ya da DB'de benzersiz kayıt) gibi düşün:

client.on('interactionCreate', async (interaction) => {
  if (!interaction.isButton()) return;
  if (interaction.customId !== 'giveaway_join') return;

  const giveaway = getGiveaway(interaction.message.id);
  if (!giveaway || giveaway.ended) {
    return interaction.reply({ content: 'Bu çekiliş artık aktif değil.', ephemeral: true });
  }

  if (giveaway.participants.includes(interaction.user.id)) {
    return interaction.reply({ content: 'Zaten katıldın! 🎉', ephemeral: true });
  }

  addParticipant(giveaway.messageId, interaction.user.id);
  await interaction.reply({ content: 'Çekilişe katıldın, bol şans!', ephemeral: true });
});

ephemeral: true sayesinde onay mesajları yalnızca tıklayan kişiye görünür ve kanal spam'le dolmaz. İstersen embed'i her katılımda güncelleyip katılımcı sayısını canlı gösterebilirsin; ancak çok kalabalık çekilişlerde sık güncelleme rate limit'e takılabileceğinden, sayacı periyodik (örneğin 5 saniyede bir) güncellemek daha sağlıklıdır.

Süreyi yönetmek: setTimeout ve toparlanma

En basit yaklaşım, çekiliş başlarken setTimeout kurup süre dolunca sonlandırma fonksiyonunu çağırmaktır. Ancak setTimeout yalnızca bellekte yaşar; bot kapanıp açılırsa zamanlayıcı yok olur. Sağlam çözüm iki katmanlıdır:

  • Çekilişin endsAt değerini kalıcı olarak sakla.
  • Bot açıldığında tüm aktif çekilişleri okuyup kalan süreye göre yeniden zamanlayıcı kur; süresi çoktan geçmiş olanları hemen sonlandır.
function scheduleEnd(giveaway) {
  const delay = giveaway.endsAt - Date.now();
  if (delay <= 0) return endGiveaway(giveaway.messageId);
  // setTimeout ~24.8 günden uzun süreleri taşıyamaz; uzun çekilişlerde
  // periyodik bir kontrol (örn. dakikada bir tarama) tercih edilir.
  setTimeout(() => endGiveaway(giveaway.messageId), delay);
}

client.once('ready', () => {
  for (const g of getActiveGiveaways()) scheduleEnd(g);
});

Çok sayıda ve çok uzun çekiliş yönetiyorsan, her biri için ayrı setTimeout yerine dakikada bir çalışan tek bir tarayıcı (cron benzeri) döngü kurup süresi dolanları toplu işlemek daha ölçeklenebilir bir yöntemdir.

Kazananı adil biçimde seçmek

Süre dolunca katılımcılar arasından rastgele kazanan(lar) seçeriz. Adillik için her katılımcının eşit şansı olmalı ve aynı kişi birden çok kez seçilmemelidir. Basit bir karıştırma (shuffle) bunu temiz biçimde sağlar:

function pickWinners(participants, count) {
  const pool = [...participants];
  // Fisher-Yates karıştırması
  for (let i = pool.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [pool[i], pool[j]] = [pool[j], pool[i]];
  }
  return pool.slice(0, Math.min(count, pool.length));
}

async function endGiveaway(messageId) {
  const giveaway = getGiveaway(messageId);
  if (!giveaway || giveaway.ended) return;
  markEnded(messageId);

  const channel = await client.channels.fetch(giveaway.channelId);
  const message = await channel.messages.fetch(messageId).catch(() => null);
  const winners = pickWinners(giveaway.participants, giveaway.winners);

  if (winners.length === 0) {
    await channel.send(`🎉 **${giveaway.prize}** çekilişi bitti ama katılan olmadı.`);
  } else {
    const mentions = winners.map(id => `<@${id}>`).join(', ');
    await channel.send(`🎉 Tebrikler ${mentions}! **${giveaway.prize}** kazandınız.`);
  }

  // Butonu devre dışı bırak
  if (message) {
    const disabled = ButtonBuilder.from(message.components[0].components[0]).setDisabled(true);
    await message.edit({ components: [new ActionRowBuilder().addComponents(disabled)] }).catch(() => {});
  }
}

Math.random() kriptografik güvenlik gerektiren bir kura için ideal değildir; ancak sıradan topluluk çekilişleri için fazlasıyla yeterli ve adildir. Kazanan çekilmediğinde (kimse katılmadıysa) net bir mesajla bunu belirtmek, üyelerin kafasının karışmasını önler.

Kalıcılık ve yeniden çekiliş

Tüm sistemin kalbi, çekiliş durumunu kalıcı tutmaktır. Küçük botlarda SQLite (better-sqlite3) idealdir: tek dosya, kurulum derdi yok. giveaways tablosunda messageId, channelId, prize, winnerCount, endsAt, ended sütunlarını; ayrı bir participants tablosunda ise messageId + userId ikilisini benzersiz indeksle saklarsın. Böylece aynı kişiyi iki kez ekleme sorununu veritabanı seviyesinde çözmüş olursun. "Yeniden çekiliş" özelliği de bu sayede kolaydır: çekiliş bittiğinde katılımcı listesini silmeyip saklarsan, bir yönetici komutuyla aynı havuzdan yeni kazanan çekebilirsin.

Sık Sorulan Sorular

Reaksiyon yerine neden buton kullanmalıyım?

Reaksiyon tabanlı çekilişlerde botun tüm reaksiyonları çekmesi gerekir, bu büyük sunucularda yavaş ve hataya açıktır; ayrıca emoji'yi kaldırıp tekrar koyarak hile yapmak kolaydır. Butonla katılımda her tıklamayı anında doğrular, katılımcıyı tek bir kaynakta (DB) tutar ve mükerrer katılımı kesin biçimde engellersin.

Bot kapanırsa devam eden çekilişler ne olur?

endsAt değerini kalıcı sakladığın sürece sorun olmaz. Bot ready olduğunda aktif çekilişleri okuyup kalan süreye göre yeniden zamanlarsın; süresi geçmiş olanları açılır açılmaz sonuçlandırırsın. Tüm durumu yalnızca bellekte tutarsan yeniden başlatma her şeyi siler.

Sadece belirli rola sahip üyeler katılabilsin nasıl yaparım?

Katılım butonunun handler'ında interaction.member.roles.cache.has(rolId) kontrolü ekleyip koşul sağlanmazsa ephemeral bir uyarıyla katılımı reddedersin. Aynı yöntemle minimum hesap yaşı veya sunucuda kalma süresi gibi şartlar da koyabilirsin.

Sunucun için sağlam bir çekiliş botu mu istiyorsun? Süreli çekilişler, rol şartları, yeniden çekiliş ve kalıcı veritabanıyla anahtar teslim bir kurulum hazırlayabilirim. Benimle iletişime geç ve ihtiyacını konuşalım.

Bu kategorideki tüm yazılar →

Devamı için