Envoyer les notifications GitHub vers Telegram avec un webhook Symfony

Illustration de Envoyer les notifications GitHub vers Telegram avec un webhook Symfony

Recevoir dans Telegram les événements GitHub d’un dépôt grâce aux composants Webhook et RemoteEvent de Symfony.

Mon équipe échange sur Telegram. Je voulais y voir les événements GitHub sans surveiller deux outils, notamment lorsqu’un collègue pousse du code. Un bot Telegram et un webhook Symfony suffisent pour relier les deux services.

Qu’est-ce qu’un webhook ?

Un webhook envoie une requête HTTP lorsqu’un événement se produit. L’application destinataire n’a donc pas besoin d’interroger régulièrement l’API source. Ici, GitHub prévient Symfony après un commit ou une pull request, puis Symfony transmet le message à Telegram.

Préparer GitHub et Telegram

Configurez d’abord un webhook sur le dépôt ou l’organisation GitHub en suivant la documentation officielle. Envoyez ensuite un message privé à BotFather pour créer le bot et obtenir son jeton d’API.

Configurer Symfony

Dans un projet Symfony existant, installez les composants Webhook et RemoteEvent, puis le client PHP de l’API Telegram auquel je contribue.

Bash
composer require symfony/webhook symfony/remote-event telegram-bot/api

Placez les deux secrets dans les variables d’environnement. En production, utilisez plutôt le système de secrets de Symfony.

Bash
# .env.local
TELEGRAM_API_TOKEN=xxxxxxxx:xxxxxxxxxxxxxxxxxxx
GITHUB_WEbHOOK_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Pour simplifier l’injection de ces valeurs, je peux les déclarer directement comme paramètres dans la configuration des services. L’autowiring me permettra ensuite d’y accéder facilement depuis n’importe quelle classe.

Avec cette configuration, Symfony injectera automatiquement les valeurs nécessaires là où j’en ai besoin :

YAML
# config/service.yaml
parameters:
    telegram_api_token: '%env(TELEGRAM_API_TOKEN)%'
    github_webhook_secret: '%env(GITHUB_WEbHOOK_SECRET)%'

Mon client API a besoin d’un jeton pour envoyer des requêtes à Telegram. Voici comment configurer une fois pour toutes l’injection de ce jeton à partir du paramètre défini plus haut :

YAML
# config/service.yaml
services:
    TelegramBot\Api\BotApi:
        arguments:
            - '%telegram_api_token%'

Créons maintenant les classes nécessaires au traitement des requêtes webhook envoyées par GitHub. Il m’en faut deux :

  • RequestParser reçoit les requêtes POST JSON de GitHub et renvoie un RemoteEvent.

  • WebhookConsumer reçoit ce RemoteEvent et exécute la logique associée.

Grâce au composant Maker de Symfony, je n’ai pas besoin de les créer manuellement. La commande suivante les génère :

Bash
php bin/console make:webhook github

Les fichiers src/RemoteEvent/GithubWebhookConsumer.php et src/Webhook/GithubRequestParser.php seront générés. Avant d’aller plus loin, configurons le webhook. Cette classe fait également office de contrôleur, et le composant Webhook crée une route /webhook/{type} associée. Dans mon cas, il s’agit de http://localhost:8000/webhook/github, l’URL à renseigner sur GitHub. Vous pouvez utiliser ngrok pour effectuer des tests dans votre environnement local.

YAML
# config/packages/webhook.yaml
framework:
    webhook:
        routing:
            github:
                service: App\Webhook\GithubRequestParser
                secret: '%github_webhook_secret%'

github_webhook_secret permet de vérifier l’origine de la requête. GitHub utilise ce secret pour produire les signatures sha1 et sha256, que le parseur valide avant de traiter l’événement.

GithubRequestParser

Cette classe comporte trois méthodes :

  • getRequestMatcher vérifie que la requête correspond au format attendu. Je peux exiger la méthode POST, un corps JSON et l’hôte github.com.

  • doParse transforme la requête en RemoteEvent.

  • validateSignature vérifie la signature avec la clé secrète.

PHP
use Symfony\Component\HttpFoundation\HeaderBag;
use Symfony\Component\HttpFoundation\ChainRequestMatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
use Symfony\Component\RemoteEvent\RemoteEvent;
use Symfony\Component\Webhook\Client\AbstractRequestParser;
use Symfony\Component\Webhook\Exception\RejectWebhookException;
use Symfony\Component\HttpFoundation\RequestMatcher\{
    MethodRequestMatcher,
    IsJsonRequestMatcher
};


final class GithubRequestParser extends AbstractRequestParser
{
    protected function getRequestMatcher(): RequestMatcherInterface
    {
        return new ChainRequestMatcher([
            new MethodRequestMatcher(Request::METHOD_POST),
            new IsJsonRequestMatcher()
        ]);
    }

    protected function doParse(
        Request $request,
        #[\SensitiveParameter] string $secret
    ): ?RemoteEvent {
        $this->validateSignature(
            headers: $request->headers,
            body: $request->getContent(),
            secret: $secret
        );

        return new RemoteEvent(
            name: $request->headers->get('X-GitHub-Event'),
            id: $request->headers->get('X-GitHub-Hook-ID'),
            payload: $request->getPayload()->all()
        );
    }

    private function validateSignature(
        HeaderBag $headers, string $body,
        #[\SensitiveParameter] string $secret
    ): void {
        $signature = hash_hmac('sha256', $body, $secret);

        if (!hash_equals($signature, $headers->get('X-Hub-Signature-256'))) {
            throw new RejectWebhookException(406, 'Invalid signature.');
        }
    }
}

GithubWebhookConsumer

Le consommateur vérifie le type d’événement GitHub, construit le message puis l’envoie au groupe avec le client Telegram. Ajoutez d’abord le bot à ce groupe. Cette réponse explique comment récupérer son ChatId.

PHP
use TelegramBot\Api\BotApi;
use Psr\Log\LoggerInterface;
use Symfony\Component\RemoteEvent\Attribute\AsRemoteEventConsumer;
use Symfony\Component\RemoteEvent\Consumer\ConsumerInterface;
use Symfony\Component\RemoteEvent\RemoteEvent;


#[AsRemoteEventConsumer('github')]
final readonly class GithubWebhookConsumer implements ConsumerInterface
{
    public function __construct(
        private BotApi $api,
        private LoggerInterface $logger
    ) {
    }

    public function consume(RemoteEvent $event): void
    {
        $name = $event->getName();

       try {
           match (true) {
               $name === 'push' => $this->handlePushEvent($event),
               $name === 'ping' => $this->handlePingEvent($event),
               default => null,
           };
       } catch (\Throwable $e) {
              $this->logger->error($e->getMessage());
       }
    }

    private function handlePushEvent(RemoteEvent $event): void
    {
        $data = $event->getPayload();
        $project = $data['repository']['full_name'];
        $pusher = $data['pusher']['name'];
        $description = $data['head_commit']['message'];
        $ref = str_replace('refs/heads/', '', $data['ref']);
        $commit = substr(strval($data['after']), 0, 8);

        $message = vsprintf(
            format: $commit === '00000000' ?
                '🔥 %s deleted %s on %s' :
                '🔥 %s pushed %s on %s : %s',
            values: [$pusher, $ref, $project, $description]
        );

        $this->sendMessage($message);
    }

    private function handlePingEvent(RemoteEvent $event): void
    {
        $data = $event->getPayload();
        $message = sprintf('👉 Github ping : %s', $data['zen']);
        $this->sendMessage($message);
    }

    private function sendMessage(?string $message = null): void
    {
        if ($message !== null) {
            $this->api->sendMessage(
                chatId: 'your group id',
                text: $message,
                disablePreview: true,
                messageThreadId: 'your topic id if any'
            );
        }
    }
}

Ce que produit ce flux

Chaque push produit maintenant une notification Telegram. Ce choix convient à une petite équipe, où le volume reste lisible. Pour un dépôt très actif, filtrez les événements dès le consommateur afin de ne pas transformer le groupe en journal technique.

Les composants Webhook et RemoteEvent prennent en charge la réception, la validation et la transformation de la requête. Le consommateur garde ainsi une seule responsabilité, construire et envoyer le message Telegram.

Articles liés