TicketWave Logo
Webhooks

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 dotenv

Alla 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
  1. Kopioi salaisuus kohdasta Dashboard → Webhooks → Endpoints ja anna se muuttujaan TICKETWAVE_WEBHOOK_SECRET.
  2. 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.js

Paina sitten päätepisteessäsi hallintapaneelissa Send test event ja seuraa konsolia.

Palvelin

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);
}

// 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:3000

Rekisterö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 toimituksenAvaa toimitus kohdassa Dashboard → Webhooks nähdäksesi tarkan pyynnön ja vastauksesi
Lähettää tapahtuman uudelleenKäytä toimituksen tietosivulla toimintoa Retry Webhook

Seuraavat vaiheet

  • Event Reference (Jokaisen tapahtuman hyötykuorma)
  • Webhooks (Allekirjoitus, uudelleenyritykset ja toimitushistoria)

How is this guide?