Ö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 dotenvAş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- Gizli anahtarı Dashboard → Webhooks → Endpoints bölümünden kopyalayın ve
TICKETWAVE_WEBHOOK_SECRETiçine ekleyin. - Makinenizde açık bir port seçin ve
PORTdeğ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.jsArdından paneldeki uç noktanızda Send test event düğmesine basın ve konsolu izleyin.
Sunucu
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:3000Yazdı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 volume | Yükü rotada bir kuyruğa gönderin ve başka yerde işleyin |
| Run several endpoints | Her birinin kendi gizli anahtarı vardır — rota başına doğru olanı seçin |
| Debug a failing delivery | Tam isteği ve yanıtınızı görmek için teslimatı Dashboard → Webhooks içinde açın |
| Re-send an event | Teslimat ayrıntı sayfasında Retry Webhook kullanın |
Sonraki Adımlar
- Event Reference (Her olayın yükü)
- Webhooks (İmzalama, yeniden denemeler ve teslimat geçmişi)
How is this guide?
