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 oder falsche ID.\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 ) { file_put_contents($this->getStatusFile(), 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; $headers = array("User-Agent: 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 => true, CURLOPT_SSL_VERIFYHOST => 2 )); $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"] ?? null; if ( empty($imageUrl) ) return false; if ( !is_dir($dirPath) ) { mkdir($dirPath, 0777, true); chmod($dirPath, 0777); } $fileName = basename(parse_url($imageUrl, PHP_URL_PATH)); $fullPath = $dirPath . $fileName; if ( file_exists($fullPath) ) return $fileName; $ch = curl_init($imageUrl); curl_setopt_array($ch, array( CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_TIMEOUT => 30, CURLOPT_USERAGENT => "YellowMastodonapi/" . self::VERSION, CURLOPT_SSL_VERIFYPEER => true, )); $imgData = curl_exec($ch); curl_close($ch); if ( !$imgData ) return false; file_put_contents($fullPath, $imgData); chmod($fullPath, 0666); 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"; } }