yellow = $yellow; $this->yellow->system->setDefault("mastodonapiInstanceUrl", "https://mastodon.social"); // Deine Mastodon Instanz-URL $this->yellow->system->setDefault("mastodonapiUserId", ""); // Deine Mastodon User ID (numerisch, z.B. 1234567890) $this->yellow->system->setDefault("mastodonapiAccessToken", ""); // Optional: Dein persönlicher API-Token (read:statuses) $this->yellow->system->setDefault("mastodonapiHashtag", ""); $this->yellow->system->setDefault("mastodonapiAuthor", "YellowUser"); $this->yellow->system->setDefault("mastodonapiTag", "Fediverse"); $this->yellow->system->setDefault("mastodonapiFolder", "content/1-blog/"); $this->yellow->system->setDefault("mastodonapiTriggerPath", "/mastodonapi-webhook-CHANGE_THIS_SECRET_KEY"); } // Handle web requests (NUR für den Webhook-Trigger) public function onRequest($scheme, $address, $base, $location, $fileName) { $triggerPath = $this->yellow->system->get("mastodonapiTriggerPath"); // Angepasster Konfigurationsname if ($location == $triggerPath) { $postsCreated = $this->processMastodonFeed(); if ($postsCreated !== false) { $this->sendResponse("Mastodon API feed processed. Created " . $postsCreated . " posts.", 200); } else { $this->sendResponse("Error processing Mastodon API feed. Check logs for details.", 500); } return 1; } return 0; } // Handle command (CLI) public function onCommand($command, $text) { if (!$this->yellow) { return 500; } switch ($command) { case "mastodonapi": // Angepasster Kommando-Name if (!empty($text)) { return 400; } $postsCreated = $this->processMastodonFeed(); if ($postsCreated !== false) { return 0; } else { return 500; } case "clean": $this->processCommandClean($command, $text); return 0; default: return 0; } } // --- Core logic for fetching and processing Mastodon API feed --- private function processMastodonFeed() { $instanceUrl = $this->yellow->system->get("mastodonapiInstanceUrl"); $userId = $this->yellow->system->get("mastodonapiUserId"); $accessToken = $this->yellow->system->get("mastodonapiAccessToken"); $hashtag = $this->yellow->system->get("mastodonapiHashtag"); $defaultAuthor = $this->yellow->system->get("mastodonapiAuthor"); $defaultTag = $this->yellow->system->get("mastodonapiTag"); $targetFolder = $this->yellow->system->get("mastodonapiFolder"); $postsCreated = 0; if (empty($instanceUrl) || empty($userId)) { $this->yellow->log("error", "MastodonAPI: 'mastodonapiInstanceUrl' or 'mastodonapiUserId' is not configured."); return false; } // API Endpunkt für die Statusse eines Benutzers $apiUrl = rtrim($instanceUrl, '/') . "/api/v1/accounts/" . $userId . "/statuses?exclude_replies=true&exclude_reblogs=true&limit=40"; $options = [ 'http' => [ 'follow_location' => 1, 'max_redirects' => 10, 'timeout' => 30, 'method' => "GET", 'header' => "User-Agent: Mozilla/5.0 (YellowCMS MastodonAPI Plugin)\r\n" ], 'ssl' => [ 'verify_peer' => true, 'verify_peer_name' => true, ], ]; // Wenn ein Access Token vorhanden ist, füge es zum Header hinzu if (!empty($accessToken)) { $options['http']['header'] .= "Authorization: Bearer " . $accessToken . "\r\n"; } $context = stream_context_create($options); $apiResponse = @file_get_contents($apiUrl, false, $context); if ($apiResponse === false) { $this->yellow->log("error", "MastodonAPI: Failed to fetch data from API. Check URL or network issues. URL: " . $apiUrl); return false; } $mastodonData = json_decode($apiResponse, true); if (json_last_error() !== JSON_ERROR_NONE) { $this->yellow->log("error", "MastodonAPI: Failed to parse JSON response: " . json_last_error_msg()); return false; } if (!is_array($mastodonData)) { $this->yellow->log("error", "MastodonAPI: API response is not an array of statuses."); return false; } $config = $this->getYellowConfig(); $yellowBaseDir = $config->get("coreServerDocument"); $mediaImagesDir = $yellowBaseDir . 'media/images/'; if (!is_dir($mediaImagesDir)) { if (!mkdir($mediaImagesDir, 0755, true)) { $this->yellow->log("error", "MastodonAPI: Could not create media images directory: " . $mediaImagesDir); } } foreach ($mastodonData as $item) { $contentHtml = $item['content']; $currentPubDate = $item['created_at']; $itemUrl = $item['url']; $imageFilenamesForMarkdown = []; // Filtern nach Hashtag (wenn konfiguriert) $hasRelevantHashtag = empty($hashtag) || (isset($item['tags']) && is_array($item['tags']) && in_array(['name' => ltrim($hashtag, '#')], array_map(function($t){ return ['name' => $t['name']]; }, $item['tags']))) || // Prüft, ob der Hashtag in den Tag-Objekten ist (strpos($contentHtml, $hashtag) !== false); if (!$hasRelevantHashtag) { continue; } $title = $this->generateTitle($contentHtml); $titleSlug = $this->generateTitleSlug($title); // Eindeutigen Dateinamen mit Toot-ID $fileName = date('Y-m-d', strtotime($currentPubDate)) . '-' . $item['id'] . '-' . $titleSlug . '.md'; $fullPathToMarkdownFile = $config->get("coreServerDocument") . $targetFolder . $fileName; if (file_exists($fullPathToMarkdownFile)) { continue; } $targetPostDir = dirname($fullPathToMarkdownFile); if (!is_dir($targetPostDir)) { if (!mkdir($targetPostDir, 0755, true)) { $this->yellow->log("error", "MastodonAPI: Could not create post directory: " . $targetPostDir); continue; } } $authorName = $item['account']['display_name'] ?: $item['account']['username']; $authorProfileUrl = $item['account']['url']; if (isset($item['media_attachments']) && is_array($item['media_attachments'])) { foreach ($item['media_attachments'] as $media) { if ($media['type'] === 'image' || $media['type'] === 'gifv') { $mediaUrl = $media['url']; $mediaAltText = $media['description'] ?? ''; $imageFilename = $this->downloadAndSaveImage($mediaUrl, $mediaImagesDir, $media['mime_type'], $mediaAltText); if ($imageFilename) { $imageFilenamesForMarkdown[] = ['filename' => $imageFilename, 'alt' => $mediaAltText]; } } } } $markdownContent = $this->generateMarkdown( $title, $titleSlug, $currentPubDate, $contentHtml, $authorName, $authorProfileUrl, $defaultAuthor, $defaultTag, $imageFilenamesForMarkdown, $itemUrl // Den ursprünglichen Toot-Link übergeben ); if (file_put_contents($fullPathToMarkdownFile, $markdownContent) === false) { $this->yellow->log("error", "MastodonAPI: Could not write Markdown file: " . $fullPathToMarkdownFile); } else { $postsCreated++; $this->yellow->log("info", "MastodonAPI: Created post: " . $fileName); } } return $postsCreated; } public function processCommandClean($command, $text) { // Implementiere hier die Logik zum Aufräumen, falls benötigt return 0; } private function downloadAndSaveImage($imageUrl, $globalImageDirectoryPath, $mediaType = '', $altText = '') { $fileName = basename(parse_url($imageUrl, PHP_URL_PATH)); $fileExtension = pathinfo($fileName, PATHINFO_EXTENSION); if (empty($fileExtension) && !empty($mediaType)) { $typeParts = explode('/', $mediaType); if (count($typeParts) > 1) { $fileExtension = $typeParts[1]; } } if (empty($fileExtension)) { $fileExtension = 'jpeg'; } $counter = 0; $uniqueFileName = pathinfo($fileName, PATHINFO_FILENAME) . '.' . $fileExtension; while (file_exists($globalImageDirectoryPath . $uniqueFileName)) { $counter++; $uniqueFileName = pathinfo($fileName, PATHINFO_FILENAME) . '-' . $counter . '.' . $fileExtension; if ($counter > 100) { $this->yellow->log("error", "MastodonAPI: Could not find unique filename for image: " . $fileName); return false; } } $finalFileName = $uniqueFileName; $fullPathToSavedImage = $globalImageDirectoryPath . $finalFileName; if (file_exists($fullPathToSavedImage)) { return $finalFileName; } $context = stream_context_create([ 'http' => [ 'follow_location' => 1, 'max_redirects' => 10, 'timeout' => 30, 'method' => "GET", 'header' => "User-Agent: Mozilla/5.0 (YellowCMS MastodonAPI Plugin)\r\n" . "Referer: " . $this->yellow->system->get("mastodonapiInstanceUrl") . "\r\n" ], 'ssl' => [ 'verify_peer' => true, 'verify_peer_name' => true, ], ]); $imageData = @file_get_contents($imageUrl, false, $context); if ($imageData === false) { $this->yellow->log("error", "MastodonAPI: Failed to download image from URL: " . $imageUrl); return false; } if (file_put_contents($fullPathToSavedImage, $imageData) === false) { $this->yellow->log("error", "MastodonAPI: Failed to save image to: " . $fullPathToSavedImage); return false; } return $finalFileName; } private function generateTitle($content) { // Da content jetzt HTML ist, strippen wir HTML, bevor wir den Titel generieren $cleanHtmlContent = strip_tags($content); $cleanContent = preg_replace('/https?:\/\/[^\s]+|#\w+/u', '', $cleanHtmlContent); // URLs und Hashtags entfernen $cleanContent = preg_replace('/[^\p{L}\p{N}\s-]/u', '', $cleanContent); // Sonderzeichen entfernen $cleanContent = trim(preg_replace('/\s+/', ' ', $cleanContent)); // Mehrere Leerzeichen durch eines ersetzen $words = explode(' ', $cleanContent); $firstWords = array_slice($words, 0, 7); // Die ersten 7 Wörter als Titel $title = implode(' ', $firstWords); if (empty($title)) { $title = "Mastodon Post"; } $finalTitle = trim(mb_substr($title, 0, 100, 'UTF-8')); return $finalTitle; } private function generateTitleSlug($title) { $slug = mb_strtolower($title, 'UTF-8'); $slug = str_replace(['ä', 'ö', 'ü', 'Ä', 'Ö', 'Ü', 'ß', ' '], ['ae', 'oe', 'ue', 'ae', 'oe', 'ue', 'ss', '-'], $slug); $slug = preg_replace('/[^a-z0-9-]+/', '', $slug); $slug = preg_replace('/-+/', '-', $slug); $slug = trim($slug, '-'); return $slug; } private function generateMarkdown($title, $titleSlug, $publishedDate, $content, $originalAuthorName, $originalAuthorProfileUrl, $yellowAuthor, $yellowTag, $imageFilenames = [], $itemUrl = '') { $date = new DateTime($publishedDate); $formattedDate = $date->format('Y-m-d'); $formattedTime = $date->format('H:i:s'); $processedContent = $content; // Ersetze Links (-Tags) im HTML-Inhalt, damit sie als Markdown-Links erscheinen. // Konvertiert TEXT zu [TEXT](URL) $processedContent = preg_replace_callback('/]*?href="([^"]*?)".*?>(.*?)<\/a>/isu', function($matches) { $url = $matches[1]; $text = strip_tags($matches[2]); // Nur den Text des Links nehmen // Wenn der Linktext dem Link selbst entspricht, nur den Link anzeigen (kürzer) if (trim($text) === trim($url) || trim($text) === 'Show more') { // "Show more" von Mastodon ignorieren return '[' . $url . ']'; } return '[' . $text . '](' . $url . ')'; }, $processedContent); // Entferne Mastodon-spezifische HTML-Hashtag-Links aus dem Content, da sie nicht als Tags // im Yellow Markdown erscheinen sollen und bereits über die Hashtag-Filterung verarbeitet wurden. // Diese Regex zielt auf Links ab, die /tags/ im href haben. $processedContent = preg_replace('/]*?href="[^"]*?\/tags\/[^"]*?".*?>#.*?<\/a>/iu', '', $processedContent); // Entfernt verbleibende HTML-Tags (z.B.

, etc.) $processedContent = strip_tags($processedContent); $processedContent = html_entity_decode($processedContent, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $processedContent = trim(preg_replace('/\s+/', ' ', $processedContent)); // Mehrere Leerzeichen durch eines ersetzen $imageMarkdown = ''; foreach ($imageFilenames as $imageData) { $imageMarkdown .= "\n[image " . $imageData['filename']; if (!empty($imageData['alt'])) { $imageMarkdown .= ' alt="' . htmlspecialchars($imageData['alt'], ENT_QUOTES | ENT_HTML5, 'UTF-8') . '"'; } $imageMarkdown .= "]\n"; } $featuredImageFrontMatter = ''; if (!empty($imageFilenames)) { $featuredImageFrontMatter = "Image: " . $imageFilenames[0]['filename'] . "\n"; } // Ersetze @ mit @ im Autorennamen für die HTML-Ausgabe $htmlSafeAuthorName = str_replace('@', '@', $originalAuthorName); $sourceLinkHtml = ''; if (!empty($itemUrl)) { $sourceLinkHtml = "
Originalbeitrag: $itemUrl"; } return << "$processedContent" > > — $htmlSafeAuthorName $sourceLinkHtml MARKDOWN; } private function getYellowConfig() { if ($this->yellow) { return $this->yellow->system; } return null; } private function sendResponse($message, $statusCode = 200) { $htmlContent = "Umleitung...

" . htmlspecialchars($message) . "

Sie werden in 3 Sekunden weitergeleitet. Falls nicht, klicken Sie hier.

"; if (!headers_sent()) { header("HTTP/1.1 " . $statusCode . " " . $this->getHttpStatusMessage($statusCode)); header("Content-Type: text/html"); } echo $htmlContent; exit; } private function getHttpStatusMessage($statusCode) { $httpStatus = array( 100 => 'Continue', 101 => 'Switching Protocols', 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 307 => 'Temporary Redirect', 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Timeout', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Payload Too Large', 414 => 'URI Too Long', 415 => 'Unsupported Media Type', 416 => 'Range Not Satisfiable', 417 => 'Expectation Failed', 426 => 'Upgrade Required', 429 => 'Too Many Requests', 431 => 'Request Header Fields Too Large', 451 => 'Unavailable For Legal Reasons', 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Timeout', 505 => 'HTTP Version Not Supported' ); return ($httpStatus[$statusCode]) ? $httpStatus[$statusCode] : 'Unknown Status'; } }