TicketWave Logo
Webhooks

Örnek Sunucu

TicketWave webhook’larını alan ve doğrulayan, çalıştırılabilir eksiksiz bir Express sunucusu.

Örnek Webhook Sunucusu

İmzaları doğrulayan, yeniden denemelere karşı koruyan, tekrarları ayıklayan ve herhangi bir işlem yapmadan önce yanıt veren, Express üzerinde minimal ama production-shaped bir alıcı.

Kopyalayın, bir uç noktayı buna yönlendirin, tamamdır.

Kurulum

Projeyi oluşturun

mkdir ticketwave-webhooks
cd ticketwave-webhooks
npm init -y
npm install express dotenv

Aşağıda Express 5 kullanılıyor, ancak kod Express 4 üzerinde de değişmeden çalışır.

Sunucuyu ekleyin

Dosyayı bir sonraki bölümden server.js olarak kaydedin.

Ortam değişkenlerinizi ayarlayın

Projenizin kök dizininde bir .env dosyası oluşturun ve aşağıdaki ortam değişkenlerini ekleyin:

# .env
TICKETWAVE_WEBHOOK_SECRET="whsec_your_secret_here"
PORT=3000
  1. Gizli anahtarı Dashboard → Webhooks → Endpoints bölümünden kopyalayın ve TICKETWAVE_WEBHOOK_SECRET içine ekleyin.
  2. Makinenizde açık bir port seçin ve PORT değişkenine yazın.

Gizli anahtarı asla server.js içine sabit olarak yazmayın veya commit etmeyin. Ona sahip olan herkes, imza kontrolünüzü geçen sahte istekler oluşturabilir.

Çalıştırın

node server.js

Ardından paneldeki uç noktanızda Send test event düğmesine basın ve konsolu izleyin.

Sunucu

server.js
const express = require('express');
const crypto = require('node:crypto');
const dotenv = require('dotenv');
dotenv.config();

const app = express();

const PORT = process.env.PORT || 3000;
const WEBHOOK_SECRET = process.env.TICKETWAVE_WEBHOOK_SECRET;

if (!WEBHOOK_SECRET) {
    console.error('Missing TICKETWAVE_WEBHOOK_SECRET');
    process.exit(1);
}

// İmza HAM gövde üzerinden oluşturulur, bu yüzden değiştirilmemiş baytları saklayın.
app.use('/webhooks/ticketwave', express.raw({ type: 'application/json' }));

// Bundan daha eski olan her şeyi reddedin; böylece yakalanan bir istek daha sonra yeniden oynatılamaz.
const MAX_TIMESTAMP_AGE = 5 * 60; // 5 minutes

function verifySignature(req) {
    const Signature = req.get('X-TicketWave-Signature');
    const Timestamp = req.get('X-TicketWave-Timestamp');

    if (!Signature || !Timestamp) return false;

    // 1. Zaman damgası güncel olmalı
    const Age = Math.abs(Math.floor(Date.now() / 1000) - Number(Timestamp));
    if (!Number.isFinite(Age) || Age > MAX_TIMESTAMP_AGE) return false;

    // 2. HMAC'i `${timestamp}.${rawBody}` üzerinde yeniden hesaplayın
    const Expected = crypto
        .createHmac('sha256', WEBHOOK_SECRET)
        .update(`${Timestamp}.${req.body}`)
        .digest('hex');

    // 3. Sabit zamanda karşılaştırın
    const Received = Signature.replace('sha256=', '');
    const ExpectedBuffer = Buffer.from(Expected, 'hex');
    const ReceivedBuffer = Buffer.from(Received, 'hex');

    if (ExpectedBuffer.length !== ReceivedBuffer.length) return false;
    return crypto.timingSafeEqual(ExpectedBuffer, ReceivedBuffer);
}

// İşlenen teslimat kimliklerini hatırlayın; çünkü bir yeniden deneme aynı olayı tekrar gönderir.
const HandledDeliveries = new Set();

app.post('/webhooks/ticketwave', (req, res) => {
    if (!verifySignature(req)) {
        console.warn('Rejected a request with an invalid signature');
        return res.status(401).json({ error: 'invalid signature' });
    }

    const DeliveryId = req.get('X-TicketWave-Delivery');
    const Payload = JSON.parse(req.body);

    // Hemen yanıt verin - 10 saniyeniz var ve yavaş yanıtlar yeniden denenir.
    res.status(200).json({ received: true });

    // Zaten işlediğimiz bir teslimatı yok sayın
    if (HandledDeliveries.has(DeliveryId)) return;
    HandledDeliveries.add(DeliveryId);

    handleEvent(Payload).catch((err) => {
        console.error(`Failed to handle ${Payload.event}:`, err);
    });
});

async function handleEvent(payload) {
    const { event, guild_id: guildId, data } = payload;

    // Paneldeki test düğmesi bunu gönderir
    if (data.test) {
        console.log(`Test event (${event}) received from guild ${guildId}`);
        return;
    }

    switch (event) {
        case 'ticket.created':
            console.log(`[${guildId}] ${data.ticket_id} opened by ${data.user?.username}`);
            break;

        case 'ticket.closed':
            console.log(`[${guildId}] ${data.ticket_id} closed by ${data.closed_by?.username} (${data.reason ?? 'no reason'})`);
            break;

        case 'ticket.updated':
            console.log(`[${guildId}] ${data.ticket_id} updated: ${data.action}`, data.changes);
            break;

        case 'message.sent':
            console.log(`[${guildId}] ${data.ticket_id} ${data.is_staff ? 'staff' : 'member'} ${data.author?.username}: ${data.content}`);
            break;

        case 'blacklist.added':
            console.log(`[${guildId}] ${data.user?.username} blacklisted (${data.reason ?? 'no reason'})`);
            break;

        case 'blacklist.removed':
            console.log(`[${guildId}] ${data.user?.username} removed from the blacklist`);
            break;

        default:
            // Yeni olay türleri zamanla eklenir - bilmediğiniz bir şeyde asla hata fırlatmayın.
            console.log(`[${guildId}] Unhandled event ${event}`);
    }
}

app.listen(PORT, () => {
    console.log(`Listening for TicketWave webhooks on port ${PORT}`);
});

Kod neden böyle görünüyor

Dört ayrıntıyı yanlış yapmak kolaydır ve dördü de sessiz başarısızlıklardır.

express.json yerine express.raw

İmza, TicketWave’in gönderdiği tam baytlar üzerinden hesaplanır. express.json() gövdeyi bir nesneye dönüştürür; onu yeniden serileştirmek anahtar sırasını veya boşlukları değiştirebilir ve özet artık eşleşmez.

Uygulamanız express.json()'u global olarak kullanıyorsa, onu webhook rotasından sonra bağlayın ya da ham ayrıştırıcıyı webhook yolu ile tam olarak yukarıda gösterildiği gibi sınırlayın. Aksi halde JSON ayrıştırıcısı kazanır ve req.body bir Buffer değil, bir nesne olur.

=== yerine timingSafeEqual

Dizeleri === ile karşılaştırmak, iki bayt farklı olduğu anda durur. Bu sürenin uzunluğu, imzanın ne kadarının doğru olduğunu sızdırır; bu da bir seferde bir baytı kaba kuvvetle denemek için yeterlidir. crypto.timingSafeEqual her zaman aynı süreyi alır.

Ayrıca iki buffer’ın uzunlukları farklıysa hata fırlatır; bu yüzden önce uzunluk kontrol edilir.

Zaman damgası kontrolü

Bu kontrol olmadan, geçerli bir isteği yakalayan biri onu sonsuza kadar yeniden oynatabilir. Zaman damgası imzalanmış yükün bir parçası olduğu için, imzayı bozmadan onu yeni bir değerle değiştirmek mümkün değildir.

Çalışmadan önce yanıt vermek

Elinizde 10 saniye var. Daha yavaş olan her şey başarısız sayılır ve yeniden denenir; bu yüzden yavaş bir veritabanı yazımı tek bir olayı üçe dönüştürür. Önce 200 yanıtını verin, sonra işi yapın.

Doğru şekilde tekrarları ayıklamak

Yukarıdaki Set bir demo için uygundur ama sonsuza kadar büyür ve yeniden başlatmadan sonra tekrar boştur. Üretimde, teslimat kimliğini kalıcı olduğu bir yerde saklayın:

// Herhangi bir SQL veritabanı ile örnek
async function alreadyHandled(deliveryId) {
    const [rows] = await db.query(
        'SELECT 1 FROM webhook_deliveries WHERE delivery_id = ?',
        [deliveryId]
    );
    if (rows.length > 0) return true;

    await db.query(
        'INSERT INTO webhook_deliveries (delivery_id) VALUES (?)',
        [deliveryId]
    );
    return false;
}

delivery_id üzerinde benzersiz bir indeks, iki yeniden deneme aynı anda geldiğinde bile bunu güvenli hale getirir.

Yerelde geliştirme

TicketWave özel veya loopback adreslerdeki uç noktaları reddeder, bu yüzden http://localhost:3000 doğrudan kullanılamaz. Yerel sunucunuzun önüne bir tünel koyun ve size verdiği herkese açık HTTPS URL’sini kaydedin:

# ngrok
ngrok http 3000

# or Cloudflare Tunnel
cloudflared tunnel --url http://localhost:3000

Yazdırılan https://….ngrok-free.app/webhooks/ticketwave URL’sini uç noktanız olarak kaydedin ve gerçek bir bilete dokunmadan önce bağlantıyı kontrol etmek için Send test event kullanın.

Ücretsiz tünel URL’leri her yeniden başlatmada değişir. Bu olduğunda paneldeki uç nokta URL’sini güncelleyin, yoksa teslimatlar başarısız olmaya başlar.

Daha İleri

Want to…Do this
Handle high message volumeYükü rotada bir kuyruğa gönderin ve başka yerde işleyin
Run several endpointsHer birinin kendi gizli anahtarı vardır — rota başına doğru olanı seçin
Debug a failing deliveryTam isteği ve yanıtınızı görmek için teslimatı Dashboard → Webhooks içinde açın
Re-send an eventTeslimat ayrıntı sayfasında Retry Webhook kullanın

Sonraki Adımlar

How is this guide?