Dieses PHP Proxy Script ermöglicht es der Mastodon Streaming API Webhook Signale an einen Webhook-Empfänger zu senden.

Anmerkung: jede Timeline die man überwachen will benötigt eine eigene Instanz dieses Scripts.

Man kann das Script ganz normal in der Konsole mit php webhook.php starten.
Oder man führt es als Service (siehe unten) aus.

Um das Script zu nutzen muss man eine Anwendung im Abschnitt Entwickung im Mastodon Backend erstellen.

webhook.php

  • {mastodon-server} - deine Mastodon Instanz (in meinem Fall social.pmj.rocks)
  • {timeline/category} - die Timeline die überwacht werden soll
  • {access-token} - das Zugriffstoken deiner Anwendung (der dritte Schlüssel)
  • {Your Webhook URL (POST)} - an diese Adresse sollen die Updates der Timeline gesendet werden (in meinem Fall eine n8n Webhook Trigger Node)
    Achtung: das Script sendet POST requests
<?php
/**
 * @copyright Copyright (C) PMJ (https://pmj.rocks)
 * @author PMJ (https://pmj.rocks)
 * @license AGPL 3.0 (http://www.gnu.org/licenses/agpl-3.0.de.html)
 * @version 1.1.0
**/

/*****
CONFIG
******/
$mastodon_server = "social.pmj.rocks";
$mastodon_stream = "public/local"; // see https://docs.joinmastodon.org/methods/streaming/#streams for more timelines
$mastodon_token  = "your public access token";
$webhook_url     = "{Your Webhook URL (POST)}";
$debug           = false; // false, "all", "file", "terminal"
/*********
END CONFIG
**********/

// Mastodon Streaming API endpoint
$mastodon_url = "https://".$mastodon_server."/api/v1/streaming/".$mastodon_stream."?access_token=".mastodon_token;

// Open a log file
$logFile = fopen("debug.log", "a");
function logMessage($message, $debug) {
  global $logFile;
  if ($debug == 'all' || $debug == 'file') {
    fwrite($logFile, "[" . date("Y-m-d H:i:s") . "] " . $message . "\n");
  }
  if ($debug == 'all' || $debug == 'terminal') {
    echo $message . "\n"; // Print to terminal
  }
}

logMessage("Starting script...", $debug);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $mastodon_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 0);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
  "Accept: text/event-stream",
  "Cache-Control: no-cache",
  "Connection: keep-alive"
]);

// SSE Buffer Handling
$buffer = "";
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($curl, $data) use ($webhook_url, &$buffer, $debug) {
  $buffer .= $data; // Append new data to buffer

  // Process full event blocks
  while (($pos = strpos($buffer, "\n\n")) !== false) { // SSE blocks end with double newline
    $eventBlock = substr($buffer, 0, $pos);
    $buffer = substr($buffer, $pos + 2); // Remove processed block from buffer

    logMessage("Raw Event Block:\n" . $eventBlock, $debug); // Debugging output

    // Extract event data
    $eventData = [];
    $lines = explode("\n", $eventBlock);
    foreach ($lines as $line) {
      $line = trim($line);

      if (strpos($line, "event:") === 0) {
        $eventData["event"] = trim(substr($line, 6));
      } elseif (strpos($line, "data:") === 0) {
        $jsonData = trim(substr($line, 5));
        $eventData["data"] = json_decode($jsonData, true);
      }
    }

    // Only send "update" events with valid data
    if (!empty($eventData["data"]) && $eventData["event"] === "update") {
      sendToWebhook($webhook_url, $eventData, $debug);
    }
  }

  return strlen($data);
});

curl_exec($ch);
curl_close($ch);
logMessage("Script ended.", $debug);
fclose($logFile);


// Function to send event data to Webhook
function sendToWebhook($webhook_url, $data, $debug) {
  global $logFile;

  $ch2 = curl_init($webhook_url);
  curl_setopt($ch2, CURLOPT_POST, true);
  curl_setopt($ch2, CURLOPT_POSTFIELDS, json_encode($data));
  curl_setopt($ch2, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
  curl_setopt($ch2, CURLOPT_RETURNTRANSFER, true);

  $response = curl_exec($ch2);
  $http_code = curl_getinfo($ch2, CURLINFO_HTTP_CODE);
  $curl_error = curl_error($ch2);
  curl_close($ch2);

  logMessage("Sent to Webhook: " . json_encode($data), $debug);
  logMessage("HTTP Status: $http_code", $debug);
  if ($curl_error) {
    logMessage("cURL Error: $curl_error", $debug);
  }
  if ($response) {
    logMessage("Webhook Response: $response", $debug);
  }
}

Diese Datei speichert man am besten an einem Ort an dem PHP ausgeführt werden kann und einem unpriviligierten Benutzer gehört.

mastodon-webhook.service

Mann kann das ganze nun wie oben erwähnt in der Konsole ausführen, oder aber als System Daemon.

Dazu erstellt man eine Datei im Ordner /etc/systemd/system mit dem Namen mastodon-webhook.service und dem Folgenden Inhalt:

[Unit]
Description=PMJ Mastodon PHP Webhook
After=network.target

[Service]
Type=simple
# The user who runs the webhook should not have special chars in its name so I use the ID
User=65535
# The working dir of the webhook
WorkingDirectory=/path/to/webhook
# Change the directory to where the bot.sh is
ExecStart=php /path/to/webhook/webhook.php
ExecReload=/bin/kill -SIGUSR1 $MAINPID
TimeoutSec=15
Restart=always

[Install]
WantedBy=multi-user.target