Bir discord ticket botu, kullanıcıların tek bir butona tıklayarak yalnızca kendileri ve destek ekibinin görebildiği özel bir kanal açmasını sağlar. Bu sayede genel sohbet karışmaz, her destek talebi ayrı bir kanalda izole şekilde yürür ve ekip taleplerin üzerinde rahatça çalışır. Bu yazıda discord.js v14 ile sıfırdan, butonla çalışan bir destek talep sistemini adım adım kuracağız.
Sistem nasıl çalışır?
Mantık üç parçadan oluşur:
- Panel mesajı: Sabit bir kanala gönderdiğimiz, "Ticket Aç" butonu içeren bir embed.
- Buton dinleyici: Kullanıcı butona bastığında bota gelen etkileşimi yakalayan ve özel kanalı oluşturan kod.
- Kapatma: Talep çözüldüğünde kanalı kapatan ikinci bir buton.
İşin püf noktası izin geçişleri (permission overwrites): kanalı oluştururken @everyone rolünden görüntüleme iznini kaldırır, sadece talebi açan kullanıcıya ve yetkili rolüne izin veririz.
Gerekli intent ve izinler
Butonlar ve kanal oluşturma için mesaj içeriğine erişmemize gerek yok; tek başına Guilds intent'i yeterlidir. Botun rolüne ise sunucu ayarlarından şu izinleri vermelisiniz: Kanalları Yönet (Manage Channels), Kanalları Görüntüle ve izin geçişlerini ayarlayabilmesi için Rolleri Yönet (Manage Roles).
const { Client, GatewayIntentBits } = require('discord.js');
const client = new Client({
intents: [GatewayIntentBits.Guilds],
});
client.once('ready', () => console.log(`Giriş yapıldı: ${client.user.tag}`));
client.login(process.env.TOKEN);
Panel mesajını gönderme
Önce kullanıcıların göreceği paneli oluşturalım. Bunu basit bir komutla ya da bot açılışında tek seferlik gönderebilirsiniz. Butonun customId değeri kritik: etkileşimi bu değerle ayırt edeceğiz.
const {
ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder,
} = require('discord.js');
const embed = new EmbedBuilder()
.setTitle('🎫 Destek Talebi')
.setDescription('Yardıma mı ihtiyacın var? Aşağıdaki butona tıklayarak sana özel bir destek kanalı aç.')
.setColor(0x5865f2);
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId('ticket_create')
.setLabel('Ticket Aç')
.setEmoji('🎫')
.setStyle(ButtonStyle.Primary),
);
await panelChannel.send({ embeds: [embed], components: [row] });
Butona basıldığında özel kanal açma
Asıl iş burada. interactionCreate olayında butonu yakalar, kullanıcının zaten açık bir talebi olup olmadığını kontrol eder ve izin geçişleriyle özel bir kanal oluştururuz. parent alanına bir kategori ID'si vererek ticket'ları tek bir kategori altında toplamak iyi bir fikirdir.
const { Events, ChannelType, PermissionFlagsBits } = require('discord.js');
const STAFF_ROLE_ID = '123456789012345678';
const TICKET_CATEGORY_ID = '987654321098765432';
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isButton()) return;
if (interaction.customId !== 'ticket_create') return;
const { guild, user } = interaction;
// Aynı kullanıcının açık talebi var mı?
const existing = guild.channels.cache.find(
(c) => c.name === `ticket-${user.id}`,
);
if (existing) {
return interaction.reply({
content: `Zaten açık bir talebin var: ${existing}`,
ephemeral: true,
});
}
const channel = await guild.channels.create({
name: `ticket-${user.id}`,
type: ChannelType.GuildText,
parent: TICKET_CATEGORY_ID,
permissionOverwrites: [
{ id: guild.id, deny: [PermissionFlagsBits.ViewChannel] },
{
id: user.id,
allow: [
PermissionFlagsBits.ViewChannel,
PermissionFlagsBits.SendMessages,
PermissionFlagsBits.AttachFiles,
],
},
{
id: STAFF_ROLE_ID,
allow: [
PermissionFlagsBits.ViewChannel,
PermissionFlagsBits.SendMessages,
],
},
],
});
const closeRow = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId('ticket_close')
.setLabel('Talebi Kapat')
.setEmoji('🔒')
.setStyle(ButtonStyle.Danger),
);
await channel.send({
content: `${user} merhaba! <@&${STAFF_ROLE_ID}> ekibi en kısa sürede ilgilenecek. Sorununu detaylıca anlatabilirsin.`,
components: [closeRow],
});
await interaction.reply({
content: `Destek kanalın oluşturuldu: ${channel}`,
ephemeral: true,
});
});
Kanal adında kullanıcı adı yerine user.id kullanmak önemli: kullanıcı adları değişebilir, özel karakter içerebilir ve mükerrer kontrolünü zorlaştırır. ID ise benzersiz ve sabittir.
Talebi kapatma
Kapatma butonu için aynı interactionCreate içine ikinci bir koşul ekliyoruz. Kullanıcıya kısa bir uyarı verip kanalı birkaç saniye sonra siliyoruz.
if (interaction.isButton() && interaction.customId === 'ticket_close') {
await interaction.reply({
content: 'Talep 5 saniye içinde kapatılıyor...',
});
setTimeout(() => {
interaction.channel.delete().catch(() => null);
}, 5000);
}
Daha olgun sistemlerde kanalı silmek yerine bir arşiv kategorisine taşıyabilir, transkripti bir log kanalına gönderebilir veya yalnızca yetkililerin onayıyla kapanmasını sağlayabilirsiniz. Başlangıç için silme yeterlidir.
Sık karşılaşılan tuzaklar
- Bot rolü yeterince yüksek değil: Botun rolü, vereceği izinleri yönetebilmesi için rol listesinde uygun bir konumda olmalı; aksi halde kanal oluşturma
Missing Permissionshatası verir. - Etkileşim zaman aşımı: Discord etkileşimlere 3 saniye içinde yanıt bekler. Kanal oluşturma uzun sürerse önce
interaction.deferReply({ ephemeral: true })çağırıp sonundainteraction.editReply(...)kullanın. - Kategori kanal limiti: Bir kategori en fazla 50 kanal alır. Yoğun sunucularda kapatılan talepleri düzenli temizleyin.
Sık Sorulan Sorular
Mesaj içeriği intent'ini açmam gerekir mi?
Hayır. Buton etkileşimleri ve kanal oluşturma için Guilds intent'i yeterlidir. MessageContent ayrıcalıklı intent'ini yalnızca kanaldaki mesajların metnini okumanız gerekiyorsa açın.
Bir kullanıcının aynı anda iki talep açmasını nasıl engellerim?
Kanal adını ticket-${user.id} biçiminde tutup, yeni talep açmadan önce önbellekte bu isimde bir kanal olup olmadığını kontrol edin. Varsa kullanıcıyı mevcut kanala yönlendirin.
Transkript (kayıt) nasıl alırım?
Kapatmadan önce kanaldaki mesajları channel.messages.fetch() ile çekip bir metin/HTML dosyasına yazabilir veya discord-html-transcripts gibi bir paket kullanıp dosyayı log kanalına gönderebilirsiniz.
Sunucunuz için profesyonel bir ticket sistemi mi istiyorsunuz? Transkript, kategori bazlı talepler, personel istatistikleri ve çok dilli arayüz dahil özel bir çözüm geliştirebilirim — benimle iletişime geçin.