Esimerkkipalvelin
Täydellinen, ajettava Express-palvelin, joka vastaanottaa ja varmistaa TicketWave-webhookit.
Esimerkkinen webhook-palvelin
Minimaalinen mutta tuotantomaisesti rakennettu vastaanotin Expressillä: se varmistaa allekirjoitukset, suojaa uudelleenlähetyksiltä, poistaa päällekkäiset yritykset ja vastaa ennen kuin tekee mitään työtä.
Kopioi se, osoita päätepiste siihen, ja siinä se.
Asennus
Luo projekti
mkdir ticketwave-webhooks
cd ticketwave-webhooks
npm init -y
npm install express dotenvAlla käytetään Express 5:tä, mutta koodi toimii sellaisenaan myös Express 4:ssä.
Lisää palvelin
Tallenna tiedosto seuraavasta osiosta nimellä server.js.
Aseta ympäristömuuttujat
Luo .env-tiedosto projektisi juureen ja lisää siihen seuraavat ympäristömuuttujat:
# .env
TICKETWAVE_WEBHOOK_SECRET="whsec_your_secret_here"
PORT=3000- Kopioi salaisuus kohdasta Dashboard → Webhooks → Endpoints ja anna se muuttujaan
TICKETWAVE_WEBHOOK_SECRET. - Valitse koneeltasi avoin portti ja aseta se
PORT-muuttujaan.
Älä koskaan kovakoodaa salaisuutta tiedostoon server.js tai commitoi sitä. Kuka tahansa, jolla on se hallussaan, voi väärentää pyyntöjä, jotka läpäisevät allekirjoitustarkistuksesi.
Käynnistä se
node server.jsPaina sitten päätepisteessäsi hallintapaneelissa Send test event ja seuraa konsolia.
Palvelin
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);
}
// Allekirjoitus muodostetaan RAAKAA rungosta, joten säilytä muuttumattomat tavut.
app.use('/webhooks/ticketwave', express.raw({ type: 'application/json' }));
// Hylkää kaiken tätä vanhemman, jotta kaapattua pyyntöä ei voi toistaa myöhemmin.
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. Aikaleiman täytyy olla tuore
const Age = Math.abs(Math.floor(Date.now() / 1000) - Number(Timestamp));
if (!Number.isFinite(Age) || Age > MAX_TIMESTAMP_AGE) return false;
// 2. Laske HMAC uudelleen muodossa `${timestamp}.${rawBody}`
const Expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(`${Timestamp}.${req.body}`)
.digest('hex');
// 3. Vertaa vakioajassa
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);
}
// Muista käsitellyt toimitus-ID:t, koska uudelleenyritys lähettää saman tapahtuman uudelleen.
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);
// Vastaa heti - sinulla on 10 sekuntia, ja hitaat vastaukset yritetään uudelleen.
res.status(200).json({ received: true });
// Ohita toimitus, jonka olemme jo käsitelleet
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;
// Hallintapaneelin testipainike lähettää tämän
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:
// Uusia tapahtumatyyppejä lisätään ajan myötä - älä koskaan heitä virhettä tuntemattomasta tapahtumasta.
console.log(`[${guildId}] Unhandled event ${event}`);
}
}
app.listen(PORT, () => {
console.log(`Listening for TicketWave webhooks on port ${PORT}`);
});Miksi koodi näyttää tältä
Neljä yksityiskohtaa on helppo tehdä väärin, ja kaikki neljä epäonnistuvat hiljaisesti.
express.raw eikä express.json
Allekirjoitus kattaa täsmälleen ne tavut, jotka TicketWave lähetti. express.json() jäsentää rungon objektiksi; sen uudelleenserialointi voi muuttaa avainten järjestystä tai välilyöntejä, eikä tiiviste enää täsmää.
Jos sovelluksesi käyttää express.json()-middlewarea globaalisti, liitä se webhook-reitin jälkeen, tai rajaa raw-parseri täsmälleen webhook-polkuun kuten yllä. Muuten JSON-parseri voittaa ja req.body on objekti, ei Buffer.
timingSafeEqual eikä ===
Merkkijonojen vertaaminen ===-operaattorilla palauttaa tuloksen heti, kun kaksi tavua eroaa. Siihen kuluva aika paljastaa, kuinka suuri osa allekirjoituksesta oli oikein, ja se riittää yhden tavun murtamiseen kerrallaan. crypto.timingSafeEqual kestää aina saman ajan.
Se myös heittää virheen, kun kahden puskurin pituudet eroavat, minkä vuoksi pituus tarkistetaan ensin.
Aikaleimatarkistus
Ilman sitä joku, joka on kaapannut kelvollisen pyynnön, voisi toistaa sen loputtomasti. Koska aikaleima on osa allekirjoitettua hyötykuormaa, sitä ei voi vaihtaa tuoreeseen rikkomatta allekirjoitusta.
Vastaa ennen työn tekemistä
Sinulla on 10 sekuntia. Kaikki hitaampi tulkitaan epäonnistumiseksi ja yritetään uudelleen, joten hidas tietokantakirjoitus muuttaa yhden tapahtuman kolmeksi. Vastaa ensin 200, tee työ sitten.
Päällekkäisyyksien poistaminen oikein
Yllä oleva Set on demoihin ihan hyvä, mutta kasvaa loputtomasti ja tyhjenee uudelleen käynnistyksen jälkeen. Tuotannossa tallenna toimitus-ID paikkaan, jossa se säilyy:
// Esimerkki millä tahansa SQL-tietokannalla
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;
}Yksilöllinen indeksi kentässä delivery_id tekee tästä turvallisen silloinkin, kun kaksi uudelleenyritystä saapuu samaan aikaan.
Paikallinen kehitys
TicketWave ei hyväksy päätepisteitä yksityisissä tai loopback-osoitteissa, joten http://localhost:3000 ei kelpaa suoraan. Laita tunneli paikallisen palvelimesi eteen ja rekisteröi sen antama julkinen HTTPS-URL:
# ngrok
ngrok http 3000
# or Cloudflare Tunnel
cloudflared tunnel --url http://localhost:3000Rekisteröi tulostettu https://….ngrok-free.app/webhooks/ticketwave-URL päätepisteeksesi ja käytä Send test event -toimintoa tarkistaaksesi kytkennän ennen kuin kosket oikeaan tikettiin.
Ilmaiset tunnel-URL-osoitteet vaihtuvat jokaisella uudelleenkäynnistyksellä. Päivitä päätepisteen URL hallintapaneelissa, kun näin käy, tai toimitukset alkavat epäonnistua.
Jatkoa varten
| Want to… | Do this |
|---|---|
| Käsitellä suurta viestimäärää | Työnnä hyötykuorma jonoon reitillä ja käsittele se muualla |
| Ajaa useita päätepisteitä | Jokaisella on oma salaisuutensa — valitse oikea reitille |
| Selvittää epäonnistuneen toimituksen | Avaa toimitus kohdassa Dashboard → Webhooks nähdäksesi tarkan pyynnön ja vastauksesi |
| Lähettää tapahtuman uudelleen | Käytä toimituksen tietosivulla toimintoa Retry Webhook |
Seuraavat vaiheet
- Event Reference (Jokaisen tapahtuman hyötykuorma)
- Webhooks (Allekirjoitus, uudelleenyritykset ja toimitushistoria)
How is this guide?
