yellow = $yellow; $this->yellow->system->setDefault("mastodonapiInstanceUrl", "https://example.social"); $this->yellow->system->setDefault("mastodonapiAccountId", ""); $this->yellow->system->setDefault("mastodonapiAccessToken", ""); // optional $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"); $this->yellow->system->setDefault("mastodonapiLimit", "20"); // neu: Anzahl Posts } // Webhook-Request public function onRequest($scheme, $address, $base, $location, $fileName) { $triggerPath = $this->yellow->system->get("mastodonapiTriggerPath"); 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; } // CLI Command public function onCommand($command, $text) { if (!$this->yellow) return 500; switch ($command) { case "mastodonapi": if (!empty($text)) return 400; $postsCreated = $this->processMastodonFeed(); return ($postsCreated !== false) ? 0 : 500; default: return 0; } } // API-Logik private function processMastodonFeed() { $instanceUrl = $this->yellow->system->get("mastodonapiInstanceUrl"); $accountId = $this->yellow->system->get("mastodonapiAccountId"); $accessToken = $this->yellow->system->get("mastodonapiAccessToken"); // optional $hashtag = ltrim($this->yellow->system->get("mastodonapiHashtag"), '#'); $defaultAuthor = $this->yellow->system->get("mastodonapiAuthor"); $defaultTag = $this->yellow->system->get("mastodonapiTag"); $targetFolder = $this->yellow->system->get("mastodonapiFolder"); $limit = intval($this->yellow->system->get("mastodonapiLimit")); if (empty($instanceUrl) || empty($accountId)) { error_log("ERROR: MastodonAPI: Missing configuration values."); return false; } $apiUrl = rtrim($instanceUrl, "/") . "/api/v1/accounts/" . $accountId . "/statuses?limit=" . $limit; $headers = [ "User-Agent: YellowMastodonAPI" ]; if (!empty($accessToken)) { $headers[] = "Authorization: Bearer " . $accessToken; } $context = stream_context_create([ 'http' => [ 'method' => "GET", 'header' => implode("\r\n", $headers) . "\r\n", 'timeout' => 30 ], 'ssl' => [ 'verify_peer' => true, 'verify_peer_name' => true ] ]); $json = @file_get_contents($apiUrl, false, $context); // Debug-Ausgabe if (isset($http_response_header)) { error_log("MastodonAPI DEBUG: HTTP Response Headers:"); foreach ($http_response_header as $header) { error_log(" " . $header); } } error_log("MastodonAPI DEBUG: Raw API Response: " . substr($json, 0, 500)); if ($json === false) { error_log("ERROR: MastodonAPI: Could not fetch API data from $apiUrl"); return false; } $posts = json_decode($json, true); if (!is_array($posts)) { error_log("ERROR: MastodonAPI: Invalid JSON returned by API."); return false; } $config = $this->getYellowConfig(); $postsCreated = 0; foreach ($posts as $post) { // Lokaler Filter: keine Boosts, keine Antworten if (!empty($post['reblog']) || (!empty($post['reblogged']) && $post['reblogged'] === true)) { continue; } if (!is_null($post['in_reply_to_id'])) { continue; } $contentHtml = $post['content']; $createdAt = $post['created_at']; $tags = array_column($post['tags'], 'name'); if (!empty($hashtag) && !in_array(strtolower($hashtag), array_map('strtolower', $tags))) { continue; } $title = $this->generateTitle($contentHtml); $titleSlug = $this->generateTitleSlug($title); $datePrefix = date('Y-m-d', strtotime($createdAt)); $fileName = $datePrefix . '-' . $titleSlug . '.md'; $fullPath = $config->get("coreServerDocument") . $targetFolder . $fileName; if (file_exists($fullPath)) continue; if (!is_dir(dirname($fullPath))) mkdir(dirname($fullPath), 0755, true); $authorName = $post['account']['display_name'] ?: $defaultAuthor; $authorUrl = $post['account']['url'] ?: "#"; // Bilder + Alt-Text $imageFilenames = []; $imageAltTexts = []; foreach ($post['media_attachments'] as $media) { if ($media['type'] === 'image' && !empty($media['url'])) { $filename = $this->downloadAndSaveImage($media['url'], $config->get("coreServerDocument") . 'media/images/'); if ($filename) { $imageFilenames[] = $filename; $imageAltTexts[] = $media['description'] ?? ''; } } } $markdownContent = $this->generateMarkdown( $title, $titleSlug, $createdAt, $contentHtml, $authorName, $authorUrl, $defaultAuthor, $defaultTag, $imageFilenames, $imageAltTexts ); file_put_contents($fullPath, $markdownContent); $postsCreated++; } return $postsCreated; } private function downloadAndSaveImage($imageUrl, $dirPath) { if (!is_dir($dirPath)) mkdir($dirPath, 0755, true); $fileName = basename(parse_url($imageUrl, PHP_URL_PATH)); $fullPath = $dirPath . $fileName; if (file_exists($fullPath)) return $fileName; $img = @file_get_contents($imageUrl); if ($img === false) return false; file_put_contents($fullPath, $img); return $fileName; } private function generateTitle($content) { $text = strip_tags($content); $text = preg_replace('/https?:\/\/[^\s]+|#\w+/u', '', $text); $text = preg_replace('/[^\p{L}\p{N}\s-]/u', '', $text); $text = trim(preg_replace('/\s+/', ' ', $text)); $words = explode(' ', $text); return trim(mb_substr(implode(' ', array_slice($words, 0, 7)), 0, 100, 'UTF-8')) ?: "Mastodon Post"; } private function generateTitleSlug($title) { $slug = mb_strtolower($title, 'UTF-8'); $slug = str_replace(['ä','ö','ü','Ä','Ö','Ü','ß',' '], ['ae','oe','ue','ae','oe','ue','ss','-'], $slug); return trim(preg_replace('/[^a-z0-9-]+/', '', $slug), '-'); } private function generateMarkdown($title, $slug, $dateTime, $html, $origAuthor, $origUrl, $yellowAuthor, $yellowTag, $images = [], $alts = []) { $date = new DateTime($dateTime); $formattedDate = $date->format('Y-m-d'); $formattedTime = $date->format('H:i:s'); $content = strip_tags($html); $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $hashtagString = ''; // Finde den gesamten Block der Tags am Ende des Strings if (preg_match('/(\s*(#\w+))+$/u', $content, $matches)) { // Entferne den Tag-Block aus dem Content $content = substr($content, 0, -strlen($matches[0])); // Finde alle einzelnen Tags im extrahierten Block if (preg_match_all('/#(\w+)/u', $matches[0], $tags)) { // Füge die Tags mit einem Leerzeichen zusammen und stelle sicher, dass sie ein # haben $hashtagString = "\n" . implode(' ', $tags[0]); } } $content = trim(preg_replace('/\s+/', ' ', $content)); $imageMarkdown = ''; foreach ($images as $i => $filename) { $alt = isset($alts[$i]) ? htmlspecialchars($alts[$i], ENT_QUOTES) : ''; $imageMarkdown .= "\n\""\n"; } $featuredImage = ''; $featuredImageAlt = ''; if (!empty($images)) { $featuredImage = "Image: " . $images[0] . "\n"; if (isset($alts[0]) && !empty($alts[0])) { $featuredImageAlt = "ImageAlt: " . htmlspecialchars($alts[0], ENT_QUOTES) . "\n"; } } $htmlSafeAuthorName = str_replace('@', '@', $origAuthor); return << "$content"$hashtagString > > — $htmlSafeAuthorName MD; } private function getYellowConfig() { return $this->yellow ? $this->yellow->system : null; } private function sendResponse($message, $statusCode = 200) { if (!headers_sent()) { header("HTTP/1.1 $statusCode"); header("Content-Type: text/html"); } echo "

" . htmlspecialchars($message) . "

"; exit; } }