Bir discord anket botu, topluluğunun karar almasını hızlandıran en kullanışlı araçlardandır: bir komutla soru ve seçenekleri girersin, üyeler seçeneklerin altındaki butonlara tıklayarak oy verir, sonuçlar da herkesin gözü önünde canlı olarak güncellenir. Bu yazıda discord.js v14 ile sıfırdan butona dayalı bir oylama sistemi kuracağız: dinamik seçenek üretimi, mükerrer oyu engelleme, yüzdelik çubuklarla canlı sonuç gösterimi ve botu yeniden başlatınca anketin kaybolmaması için kalıcılık. Eski emoji (reaksiyon) yöntemi yerine butonları kullanacağız, çünkü daha temiz, daha güvenilir ve hileye daha kapalıdır.
Anket sisteminin mimarisi
Bir anket özünde iki şeyi takip eder: hangi seçenekler var ve kim hangi seçeneğe oy verdi. Her aktif anket için şu bilgileri saklaman gerekir:
- Anket mesajının kanal ve mesaj kimliği (
messageId). - Soru metni ve seçenekler dizisi.
- Her oyun sahibi:
userId→ seçilen seçenek indeksi eşlemesi. - Anketin tek mi yoksa çok mu seçimli olduğu ve isteğe bağlı bitiş zamanı.
Bu durumu yalnızca bellekte (Map) tutarsan bot her yeniden başladığında tüm aktif anketler kaybolur. Bu yüzden ileride kalıcı bir depo (SQLite gibi) ekleyeceğiz; ama önce mantığı net kurup sonra kalıcılığı bağlayacağız.
Anketi başlatan slash komutu
Anketi bir slash komutla başlatıyoruz. Discord bir butona en fazla beş bileşen sığdırdığından (bir ActionRow başına beş buton), pratikte tek satırda beşe kadar seçenek koyabilirsin; daha fazlası için ikinci bir satır eklenir. Burada virgülle ayrılmış seçenekleri alıp dinamik buton üretelim:
const {
SlashCommandBuilder,
EmbedBuilder,
ButtonBuilder,
ButtonStyle,
ActionRowBuilder,
} = require('discord.js');
const data = new SlashCommandBuilder()
.setName('anket')
.setDescription('Yeni bir anket başlat')
.addStringOption(o =>
o.setName('soru').setDescription('Anket sorusu').setRequired(true))
.addStringOption(o =>
o.setName('secenekler').setDescription('Virgülle ayır: Evet, Hayir, Belki').setRequired(true));
async function execute(interaction) {
const question = interaction.options.getString('soru');
const options = interaction.options.getString('secenekler')
.split(',').map(s => s.trim()).filter(Boolean).slice(0, 5);
if (options.length < 2) {
return interaction.reply({ content: 'En az iki seçenek gerekli.', ephemeral: true });
}
const row = new ActionRowBuilder().addComponents(
options.map((label, i) =>
new ButtonBuilder()
.setCustomId(`poll_vote_${i}`)
.setLabel(label.slice(0, 80))
.setStyle(ButtonStyle.Secondary))
);
const message = await interaction.reply({
embeds: [buildPollEmbed(question, options, {})],
components: [row],
fetchReply: true,
});
createPoll({ messageId: message.id, question, options, votes: {} });
}
Her butonun customId değerine seçeneğin indeksini gömüyoruz (poll_vote_0, poll_vote_1...). Böylece tıklamayı işlerken kullanıcının hangi seçeneği seçtiğini doğrudan okuyabiliriz. Etiketleri 80 karaktere kesiyoruz çünkü Discord buton etiketleri için sınır koyar.
Butonlarla oy toplama ve mükerrer oyu engelleme
Bir butona her tıklandığında o kullanıcının oyunu kaydederiz. Kritik nokta: aynı kişi yeniden tıklarsa eski oyu silinip yenisi yazılmalı, oy sayısı şişmemelidir. Oyları userId anahtarlı bir nesnede tuttuğumuz için bu doğal olarak çözülür; tekrar yazmak eskisinin üzerine biner:
client.on('interactionCreate', async (interaction) => {
if (!interaction.isButton()) return;
if (!interaction.customId.startsWith('poll_vote_')) return;
const poll = getPoll(interaction.message.id);
if (!poll || poll.ended) {
return interaction.reply({ content: 'Bu anket artık aktif değil.', ephemeral: true });
}
const choice = parseInt(interaction.customId.split('_')[2], 10);
const previous = poll.votes[interaction.user.id];
setVote(poll.messageId, interaction.user.id, choice);
const msg = previous === choice
? 'Oyun zaten bu seçenekteydi.'
: `Oyun kaydedildi: **${poll.options[choice]}**`;
// Embed'i güncel sonuçlarla yenile
await interaction.update({
embeds: [buildPollEmbed(poll.question, poll.options, poll.votes)],
});
await interaction.followUp({ content: msg, ephemeral: true });
});
Burada interaction.update() ile anket mesajının kendisini güncel sonuçlarla yeniliyoruz; ardından followUp ile yalnızca oy verene görünen ephemeral bir onay gönderiyoruz. Böylece kanal teşekkür mesajlarıyla dolmaz, sonuçlar da herkes için canlı kalır.
Sonuçları canlı gösterme
Anketin can alıcı kısmı sonuç sunumudur. Embed içinde her seçenek için oy sayısını, yüzdeyi ve metinden çizilmiş basit bir çubuk göstereceğiz. Önce oyları sayalım, sonra çubuğu kuralım:
function buildPollEmbed(question, options, votes) {
// votes: { userId: optionIndex }
const counts = options.map((_, i) =>
Object.values(votes).filter(v => v === i).length);
const total = counts.reduce((a, b) => a + b, 0);
const lines = options.map((label, i) => {
const count = counts[i];
const pct = total ? Math.round((count / total) * 100) : 0;
const filled = Math.round(pct / 10); // 10 bloklu çubuk
const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
return `**${label}**\n${bar} ${pct}% (${count})`;
});
return new EmbedBuilder()
.setTitle(`📊 ${question}`)
.setDescription(lines.join('\n\n'))
.setFooter({ text: `Toplam oy: ${total}` })
.setColor(0x5865f2);
}
█ ve ░ blok karakterleriyle çizdiğimiz çubuk her platformda aynı genişlikte görünür ve harici bir görsel kütüphanesine ihtiyaç bırakmaz. Yüzdeyi her zaman toplam oya göre hesaplamak önemli; total sıfırken bölme hatasını total ? ... : 0 koşuluyla önlüyoruz.
Çok seçimli anketler ve anketi kapatma
Şimdiye kadar her kullanıcı tek seçenek seçebiliyordu. Çok seçimli (birden fazla seçeneğe oy) bir anket istiyorsan veri yapısını userId → indeks dizisi şeklinde değiştirip butona her tıklamada o seçeneği diziye ekler ya da diziden çıkarırsın (toggle). Tek seçimli sürüm çoğu topluluk anketi için yeterlidir ve sonuçların yorumlanmasını kolaylaştırır.
Anketi kapatmak için iki yol vardır: anket sahibine bir "Anketi bitir" komutu sunmak ya da isteğe bağlı bir süre verip dolduğunda otomatik kapatmak. Kapatırken butonları devre dışı bırakıp son sonucu sabitlemek temiz bir bitiş sağlar:
async function endPoll(messageId) {
const poll = getPoll(messageId);
if (!poll || poll.ended) return;
markEnded(messageId);
const channel = await client.channels.fetch(poll.channelId);
const message = await channel.messages.fetch(messageId).catch(() => null);
if (!message) return;
// Butonların hepsini devre dışı bırak
const disabledRows = message.components.map(row =>
new ActionRowBuilder().addComponents(
row.components.map(c => ButtonBuilder.from(c).setDisabled(true))));
const embed = buildPollEmbed(poll.question, poll.options, poll.votes)
.setTitle(`📊 ${poll.question} (kapandı)`);
await message.edit({ embeds: [embed], components: disabledRows });
}
Kalıcılık: anketleri yeniden başlatmaya dayanıklı yapmak
Tüm sistemin kalbi, anket durumunu kalıcı tutmaktır. Küçük botlarda SQLite (better-sqlite3) idealdir: tek dosya, kurulum derdi yok. İki tablo yeterlidir: polls tablosunda messageId, channelId, question, options (JSON metni), ended; ayrı bir votes tablosunda ise messageId + userId ikilisini benzersiz (UNIQUE) indeksle, yanında choice sütunuyla saklarsın. Benzersiz indeks sayesinde aynı kullanıcının ikinci oyu, INSERT ... ON CONFLICT ile sessizce eskisinin üzerine yazılır; mükerrer oy sorunu veritabanı seviyesinde çözülür. Bot ready olduğunda aktif anketleri okuyarak hafızadaki Map'i yeniden doldurursun ve hiçbir oy kaybolmaz.
Sık Sorulan Sorular
Reaksiyon yerine neden buton kullanmalıyım?
Reaksiyon tabanlı anketlerde botun tüm reaksiyonları çekmesi gerekir; bu büyük sunucularda yavaştır, hataya açıktır ve emoji'yi kaldırıp tekrar koyarak hile yapmak kolaydır. Butonla oylamada her tıklamayı anında doğrular, oyu tek bir kaynakta (DB) tutar ve mükerrer oyu kesin biçimde engellersin. Ayrıca customId ile hangi seçeneğin seçildiğini kuşkuya yer bırakmadan okursun.
Discord'un yeni yerleşik anket özelliğini mi kullanmalıyım?
Basit, hızlı anketler için Discord'un yerleşik anketi gayet iyidir. Ancak rol bazlı kısıtlama, sonuçları başka bir kanala loglama, özel sonuç görselleri veya verileri kendi veritabanında saklama gibi ihtiyaçların varsa kendi botun sana tam kontrol verir. Bu yazıdaki yaklaşım tam da bu esnekliği hedefler.
Oyların gizli kalmasını nasıl sağlarım?
Embed'de yalnızca toplam sayıları ve yüzdeleri gösterip kimin ne oy verdiğini hiçbir yerde yayımlamazsın. Onay mesajlarını ephemeral: true ile gönderdiğinden, bir kullanıcının seçimi yalnızca kendisine görünür. Kim hangi oyu verdi bilgisini sadece veritabanında tutar, kanala asla yansıtmazsın.
Sunucun için sağlam bir anket botu mu istiyorsun? Butonlu oylama, canlı sonuç çubukları, rol şartları ve kalıcı veritabanıyla anahtar teslim bir kurulum hazırlayabilirim. Benimle iletişime geç ve ihtiyacını konuşalım.