Découverte de l'API EventSub Twitch

Voir la vidéo

Dans ce tutoriel, je vous propose de découvrir comment utiliser l'API EventSub de Twitch afin de détecter quand un live est commencé ou terminé.

Liens utiles

Comme d'habitude, lorsque l'on utilise une API, le principal problème est de trouver les informations afin de réaliser ce que l'on souhaite faire. Aussi, voici une liste des liens importants liés à l'utilisation de l'API EventSub.

Fonctionnement général

L'objectif de notre exercice est de mettre en place un système qui va nous permettre de détecter quand un Stream est commencé ou terminé. Pour mettre en place un tel système, il faudra créer un abonnement auprès de l'API EventSub qui permettra d'enregistrer un webhook. Ce webhook se présente sous forme d'une URL qui sera appelée par Twitch lorsque certains événements seront déclenchés (dans notre cas les évènements stream.online et stream.offline).

Ensuite, il faudra faire en sorte que notre serveur réponde correctement à l'appel de ce webhook afin de gérer la logique à mettre en place.

Création de l'application Twitch

La première étape consiste donc à créer notre abonnement. Pour cela, on va commencer par se rendre sur l'espace développeur de Twitch afin de créer une application.

  • OAuth Redirect URLs : on n'utilisera pas cette option, donc vous pouvez mettre n'importe quel URL.
  • Client Type : on choisira l'option confidentielle étant donné que notre API ne sera utilisée que par notre serveur.

On obtiendra alors deux informations qu'il vous faudra conserver le Client ID et le Client Secret.

Obtention du jeton d'accès

Avec ses identifiants en poche il est maintenant possible d'obtenir un jeton d'accès. L'API Twitch utilise un système d'authentification basé sur l'Oauth. Dans notre cas, on pourra utiliser une authentification de type client_credentials afin d'obtenir un jeton de niveau application. Ce jeton a une permission suffisante pour ce que l'on souhaite faire.

POST https://id.twitch.tv/oauth2/token
Content-Type: application/x-www-form-urlencoded
Accept: application/json

client_id={{TWITCH_ID}}&client_secret={{TWITCH_SECRET}}&grant_type=client_credentials

En PHP (avec le client HTTP de Symfony)

private function getAccessToken(): string
{
    $response = $this->client->request('POST', 'https://id.twitch.tv/oauth2/token', [
        'body' => [
            'client_id' => $this->clientID,
            'client_secret' => $this->clientSecret,
            'grant_type' => 'client_credentials'
        ]
    ]);
    if ($response->getStatusCode() >= 300) {
        throw new \Exception("Cannot get access token : \n" . $response->getContent(false));
    }
    return $response->toArray()['access_token'];
}

Enregistrement des abonnements

Avec le jeton d'accès on peut maintenant appeler l'API EventSub afin d'enregistrer notre webhook.

POST https://api.twitch.tv/helix/eventsub/subscriptions
Content-Type: application/json
Accept: application/json
Client-ID: {{TWITCH_ID}}
Authorization: Bearer {{access_token}}

{
  "type": "stream.online",
  "version": "1",
  "condition": {
    "broadcaster_user_id": "677850539"
  },
  "transport": {
    "method": "webhook",
    "callback": "https://grafitwitch.loca.lt/twitch/webhook",
    "secret": "mysecretwithlotsofchars"
  }
}

Il y a plusieurs éléments importants :

  • Le broadcaster_user_id doit contenir l'ID de la chaîne pour laquelle on veut détecter l'évènement. Pour obtenir cet ID à partir du nom utilisateur vous pouvez utiliser des services tiers.
  • Le callback doit être une URL valide atteignable depuis l'extérieur qui répondra à une requête de type POST (on verra cela dans la suite).
  • Le secret est une clef secrète, qui ne devra être connu que par votre serveur, et qui permettra d'authentifier l'appel au webhook plus tard.

Aussi, avant d'appeler cette API il est important de préparer la page (callback) qui recevra le webhook car lors de l'enregistrement Twitch vérifiera que vous êtes le propriétaire de l'URL en envoyant un challenge.

public function createSubscriptions(): void
{
    $events = ['stream.online', 'stream.offline'];
    $accessToken = $this->getAccessToken();
    foreach ($events as $event) {
        $response = $this->client->request('POST', 'https://api.twitch.tv/helix/eventsub/subscriptions', [
            'headers' => [
                'Authorization' => 'Bearer ' . $accessToken,
                'Client-ID' => $this->clientID
            ],
            'json' => [
                'type' => $event,
                'version' => '1',
                'condition' => [
                    'broadcaster_user_id' => $this->broadcasterId
                ],
                'transport' => [
                    'method' => 'webhook',
                    'callback' => 'https://grafitwitch.loca.lt/twitch/webhook',
                    'secret' => $this->channelSecret
                ]
            ]
        ]);
        if ($response->getStatusCode() >= 300) {
            throw new \Exception("Cannot add twitch subscription : \n" . $response->getContent(false));
        }
    }
}

Répondre au challenge

Lorsque vous allez enregistrer votre webhook avec l'API, Twitch va contacter votre serveur afin de vérifier que vous êtes bien le propriétaire de l'URL que vous avez soumis. Afin de savoir si la requête est de type Challenge, il faudra regarder les en-têtes reçues.

Valider la signature

Aussi, lorsque l'on reçoit un appel à la partie webbook, il est important de valider la signature de l'appel (la documentation propose d'ailleurs un petit exemple en javascript). C'est dans le cadre de cette signature que l'on utilisera la propriété secrète qui a été envoyée lors de l'enregistrement du webhook.

public function validateSignature(Request $request): bool
{
    $signature = $request->headers->get('Twitch-Eventsub-Message-Signature');
    $messageId = $request->headers->get('Twitch-Eventsub-Message-Id');
    $timestamp = $request->headers->get('Twitch-Eventsub-Message-Timestamp');
    $content = $request->getContent();
    $message = $messageId . $timestamp . $content;
    $expectedSignature = 'sha256=' . hash_hmac('sha256', $message, $this->channelSecret);
    return hash_equals($expectedSignature, $signature);
}

Une fois cette signature vérifiée, on va pouvoir répondre au challenge en renvoyant dans notre réponse le contenu du challenge que l'on a reçu dans la requête.

if (!$api->validateSignature($request)) {
    throw new \Exception('Signature du webhook incorrect');
}
$isVerification = $request->headers->get('Twitch-Eventsub-Message-Type') === 'webhook_callback_verification';
$body = json_decode($request->getContent(), true);
if ($isVerification) {
    return new Response($body['challenge'], 200);
}

Ensuite, il est possible d'ajouter du code pour gérer les différents événements supplémentaires que l'on va recevoir.

#[Route('/twitch/webhook', methods: ['POST'])]
public function webhook(Request $request, TwitchAPI $api, OptionRepository $repository)
{
    if (!$api->validateSignature($request)) {
        throw new \Exception('Signature du webhook incorrect');
    }
    $isVerification = $request->headers->get('Twitch-Eventsub-Message-Type') === 'webhook_callback_verification';
    $body = json_decode($request->getContent(), true);
    if ($isVerification) {
        return new Response($body['challenge'], 200);
    }

    if ($body['subscription']['type'] === 'stream.online') {
        $repository->updateLiveDate(new \DateTimeImmutable());
        return new Response('', 204);
    }
    if ($body['subscription']['type'] === 'stream.offline') {
        $repository->updateLiveDate(null);
        return new Response('', 204);
    }

    throw new \Exception('Unhandled webhook event ' . $request->headers->get('Twitch-Eventsub-Subscription-Type'));
}

Et voilà, le tour est joué, On est capable de détecter lorsque l'on est en live ou non et de sauvegarder l'information dans notre base de données.

Publié
Technologies utilisées
Auteur :
Grafikart
Partager