yellow = $yellow; $this->yellow->system->setDefault("mastodonapiInstanceUrl", "https://example.social"); $this->yellow->system->setDefault("mastodonapiAccountId", ""); $this->yellow->system->setDefault("mastodonapiAccessToken", ""); $this->yellow->system->setDefault("mastodonapiUpdateInterval", "30"); $this->yellow->system->setDefault("mastodonapiLimit", "20"); $this->yellow->system->setDefault("mastodonapiHashtag", ""); $this->yellow->system->setDefault("mastodonapiAuthor", "YellowUser"); $this->yellow->system->setDefault("mastodonapiTag", "Fediverse"); $this->yellow->system->setDefault("mastodonapiFolder", "content/2-blog/"); $this->yellow->system->setDefault("mastodonapiMaxAge", "0"); $this->yellow->system->setDefault("mastodonapiStatus", "public"); } // Handle update public function onUpdate($action) { if ( $action=="mastodon" || $action=="daily" || $action=="install" ) { $this->updateMastodonPosts(); } } // Handle command public function onCommand($command, $text) { if ( $command=="mastodonapi" ) { echo "Mastodon Import gestartet...\n"; $count = $this->updateMastodonPosts(); if ( $count===false ) { echo "Fehler: API nicht erreichbar, falsche ID oder Zugriff verweigert.\n"; return 500; } echo "Erfolg: " . $count . " neue Posts verarbeitet.\n"; return true; } return 0; } // Handle periodic update public function onTick() { $interval = intval($this->yellow->system->get("mastodonapiUpdateInterval")) * 60; $statusFile = $this->getStatusFile(); $lastUpdate = file_exists($statusFile) ? intval(file_get_contents($statusFile)) : 0; if ( time() - $lastUpdate >= $interval ) { $this->updateMastodonPosts(); } } // Update Mastodon posts public function updateMastodonPosts() { $postsCreated = $this->processMastodonFeed(); if ( $postsCreated!==false ) { $statusFile = $this->getStatusFile(); file_put_contents($statusFile, time()); } return $postsCreated; } // Process Mastodon feed and create pages private function processMastodonFeed() { $instanceUrl = $this->yellow->system->get("mastodonapiInstanceUrl"); $accountId = $this->yellow->system->get("mastodonapiAccountId"); $accessToken = $this->yellow->system->get("mastodonapiAccessToken"); $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"); $statusSetting = $this->yellow->system->get("mastodonapiStatus"); $maxAgeHours = intval($this->yellow->system->get("mastodonapiMaxAge")); $limit = intval($this->yellow->system->get("mastodonapiLimit")); if ( empty($instanceUrl) || empty($accountId) ) return false; $apiUrl = rtrim($instanceUrl, "/") . "/api/v1/accounts/" . $accountId . "/statuses?limit=" . $limit; // FIX: Browser-ähnlicher User-Agent, da Snac strikt blockiert $headers = array("User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) YellowMastodonapi/" . self::VERSION); if ( !empty($accessToken) ) $headers[] = "Authorization: Bearer " . $accessToken; $ch = curl_init($apiUrl); curl_setopt_array($ch, array( CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_TIMEOUT => 30, CURLOPT_HTTPHEADER => $headers, CURLOPT_SSL_VERIFYPEER => false, // FIX: Kompatibilität für GoToSocial/Snac erhöht CURLOPT_SSL_VERIFYHOST => 0 )); $json = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ( $json===false || $httpCode < 200 || $httpCode >= 300 ) return false; $posts = json_decode($json, true); if ( !is_array($posts) ) return false; $postsCreated = 0; foreach ( $posts as $post ) { 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"] ?? ""; if ( $maxAgeHours > 0 ) { if ( strtotime($createdAt) < ( time() - ( $maxAgeHours * 3600 ) ) ) continue; } $tags = array_column($post["tags"] ?? array(), "name"); if ( !empty($hashtag) && !in_array(strtolower($hashtag), array_map("strtolower", $tags)) ) continue; $title = $this->generateTitle($contentHtml); $titleSlug = $this->generateTitleSlug($title); $fileName = date("Y-m-d", strtotime($createdAt)) . "-" . $titleSlug . ".md"; $fullPath = $this->yellow->system->get("coreServerDocument") . $targetFolder . $fileName; if ( file_exists($fullPath) ) continue; if ( !is_dir(dirname($fullPath)) ) { mkdir(dirname($fullPath), 0777, true); chmod(dirname($fullPath), 0777); } $imageFilenames = array(); $imageAltTexts = array(); foreach ( $post["media_attachments"] ?? array() as $media ) { if ( ( $media["type"] ?? "" )==="image" ) { $filename = $this->downloadAndSaveImage($media, $this->yellow->system->get("coreServerDocument") . "media/images/"); if ( $filename ) { $imageFilenames[] = $filename; $imageAltTexts[] = $media["description"] ?? ""; } } } $markdownContent = $this->generateMarkdown( $title, $titleSlug, $createdAt, $contentHtml, $post["account"]["display_name"] ?: $defaultAuthor, $post["account"]["url"] ?: "#", $defaultAuthor, $defaultTag, $imageFilenames, $imageAltTexts, $statusSetting ); file_put_contents($fullPath, $markdownContent); chmod($fullPath, 0666); $postsCreated++; } return $postsCreated; } // Download and save image private function downloadAndSaveImage($media, $dirPath) { $imageUrl = $media['remote_url'] ?? $media['url'] ?? $media['preview_url'] ?? ($media['meta']['original']['url'] ?? null) ?? ($media['meta']['small']['url'] ?? null); if (empty($imageUrl)) return false; if (!is_dir($dirPath)) mkdir($dirPath, 0755, true); $fileName = basename(parse_url($imageUrl, PHP_URL_PATH)); if (!strpos($fileName, '.')) { $fileName .= ".jpg"; } $fullPath = $dirPath . $fileName; if (file_exists($fullPath) && filesize($fullPath) > 0) return $fileName; $ch = curl_init($imageUrl); $headers = ["User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) YellowMastodonAPI"]; $accessToken = $this->yellow->system->get("mastodonapiAccessToken"); if (!empty($accessToken)) { $headers[] = "Authorization: Bearer " . $accessToken; } curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_TIMEOUT => 30, CURLOPT_HTTPHEADER => $headers, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => 0 ]); $imgData = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($imgData === false || $httpCode < 200 || $httpCode >= 300) { return false; } file_put_contents($fullPath, $imgData); return $fileName; } // Generate title from content private function generateTitle($content) { $text = trim(strip_tags($content)); $text = preg_replace("/https?:\/\/[^\s]+|#\w+/u", "", $text); $words = explode(" ", $text); return trim(mb_substr(implode(" ", array_slice($words, 0, 7)), 0, 100, "UTF-8")) ?: "Mastodon Post"; } // Generate slug from title private function generateTitleSlug($title) { $slug = mb_strtolower($title, "UTF-8"); $slug = str_replace(array("ä","ö","ü","ß"," "), array("ae","oe","ue","ss","-"), $slug); return trim(preg_replace("/[^a-z0-9-]+/", "", $slug), "-"); } // Generate markdown content private function generateMarkdown($title, $slug, $dateTime, $html, $origAuthor, $origUrl, $yellowAuthor, $yellowTag, $images, $alts, $status) { $date = new DateTime($dateTime); $pub = $date->format("Y-m-d H:i:s"); $content = trim(html_entity_decode(strip_tags(str_replace(array("
", "

"), array("\n", "

\n"), $html)), ENT_QUOTES | ENT_HTML5, "UTF-8")); $content = str_replace(array("\n", "#"), array("\n> ", "#"), $content); $imageMarkdown = ""; $featuredImage = $featuredImageAlt = ""; foreach ( $images as $i => $filename ) { $alt = isset($alts[$i]) ? htmlspecialchars($alts[$i], ENT_QUOTES) : ""; $imageMarkdown .= "\n\"$alt\"/\n"; if ( $i===0 ) { $featuredImage = "Image: $filename\n"; $featuredImageAlt = "ImageAlt: $alt\n"; } } $authorSafe = str_replace("@", "@", $origAuthor); return "---\n" . "Title: $title\n" . "TitleSlug: $slug\n" . "Published: $pub\n" . "Author: $yellowAuthor\n" . "Layout: blog\n" . "Tag: $yellowTag\n" . "Status: $status\n" . $featuredImage . $featuredImageAlt . "---\n\n" . $imageMarkdown . "\n" . "> " . $content . "\n>\n" . "> — $authorSafe"; } // Return status file path private function getStatusFile() { return $this->yellow->system->get("coreWorkerDirectory") . "mastodonapi.status"; } }