diff --git a/mastodonapi.php b/mastodonapi.php index ed88b13..81dd9d3 100644 --- a/mastodonapi.php +++ b/mastodonapi.php @@ -1,121 +1,225 @@ yellow = $yellow; - $yellow->system->setDefault("mastodonapiInstance", "https://mastodon.social"); - $yellow->system->setDefault("mastodonapiUserId", ""); - $yellow->system->setDefault("mastodonapiToken", ""); - $yellow->system->setDefault("mastodonapiHashtag", ""); // Nur Posts mit diesem Hashtag werden importiert - $yellow->system->setDefault("mastodonapiTag", "Fediverse"); // Dieser Tag wird im Yellow CMS gesetzt - $yellow->system->setDefault("mastodonapiAuthor", "YellowUser"); - $yellow->system->setDefault("mastodonapiFolder", "content/1-blog/"); - $yellow->system->setDefault("mastodonapiTriggerPath", "/mastodonapi-webhook-CHANGE_THIS_SECRET_KEY"); + $this->yellow->system->setDefault("mastodonapiInstanceUrl", "https://example.social"); + $this->yellow->system->setDefault("mastodonapiAccountId", ""); + $this->yellow->system->setDefault("mastodonapiAccessToken", ""); + $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"); } + // Webhook-Request public function onRequest($scheme, $address, $base, $location, $fileName) { $triggerPath = $this->yellow->system->get("mastodonapiTriggerPath"); if ($location == $triggerPath) { - $postsCreated = $this->processMastodonAPI(); + $postsCreated = $this->processMastodonFeed(); if ($postsCreated !== false) { - $this->sendResponse("Mastodon API processed. Created $postsCreated posts.", 200); + $this->sendResponse("Mastodon API feed processed. Created " . $postsCreated . " posts.", 200); } else { - $this->sendResponse("Error processing Mastodon API. Check logs.", 500); + $this->sendResponse("Error processing Mastodon API feed. Check logs for details.", 500); } return 1; } return 0; } + // CLI Command public function onCommand($command, $text) { - if ($command == "mastodonapi") { - $postsCreated = $this->processMastodonAPI(); - return $postsCreated !== false ? 0 : 500; + 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; } - return 0; } - private function processMastodonAPI() { - $instance = rtrim($this->yellow->system->get("mastodonapiInstance"), "/"); - $userId = $this->yellow->system->get("mastodonapiUserId"); - $token = $this->yellow->system->get("mastodonapiToken"); - $hashtag = $this->yellow->system->get("mastodonapiHashtag"); - $tag = $this->yellow->system->get("mastodonapiTag"); - $author = $this->yellow->system->get("mastodonapiAuthor"); - $folder = $this->yellow->system->get("mastodonapiFolder"); + // API-Logik + 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"); - if (empty($userId) || empty($token)) { - error_log("ERROR: MastodonAPI: Missing user ID or token."); + if (empty($instanceUrl) || empty($accountId) || empty($accessToken)) { + error_log("ERROR: MastodonAPI: Missing configuration values."); return false; } - $url = "$instance/api/v1/accounts/$userId/statuses?exclude_replies=true&exclude_reblogs=true&limit=5"; - $opts = [ - "http" => [ - "method" => "GET", - "header" => "Authorization: Bearer $token\r\nUser-Agent: YellowCMS\r\n" + $apiUrl = rtrim($instanceUrl, "/") . "/api/v1/accounts/" . $accountId . "/statuses?limit=20"; + $context = stream_context_create([ + 'http' => [ + 'method' => "GET", + 'header' => "Authorization: Bearer " . $accessToken . "\r\n" . + "User-Agent: YellowMastodonAPI\r\n", + 'timeout' => 30 + ], + 'ssl' => [ + 'verify_peer' => true, + 'verify_peer_name' => true ] - ]; - $context = stream_context_create($opts); - $json = @file_get_contents($url, false, $context); + ]); + + $json = @file_get_contents($apiUrl, false, $context); if ($json === false) { - error_log("ERROR: MastodonAPI: Failed to fetch API data."); + error_log("ERROR: MastodonAPI: Could not fetch API data from $apiUrl"); return false; } - $statuses = json_decode($json, true); - if (!is_array($statuses)) { - error_log("ERROR: MastodonAPI: Invalid JSON response."); + $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 ($statuses as $status) { - // Nur Posts mit dem gewünschten Hashtag importieren - if (!empty($hashtag) && stripos($status["content"], "#$hashtag") === false) { + + foreach ($posts as $post) { + $contentHtml = $post['content']; + $createdAt = $post['created_at']; + $tags = array_column($post['tags'], 'name'); + + if (!empty($hashtag) && !in_array($hashtag, array_map('strtolower', $tags))) { continue; } - $content = strip_tags($status["content"]); - $createdAt = $status["created_at"]; - $title = mb_substr(preg_replace("/\\s+/", " ", $content), 0, 50); - $slug = strtolower(preg_replace("/[^a-z0-9]+/", "-", $title)); - $datePrefix = date("Y-m-d", strtotime($createdAt)); - $fileName = "$datePrefix-$slug.md"; - $path = $this->yellow->system->get("coreServerDocument") . $folder . $fileName; + $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($path)) continue; + if (file_exists($fullPath)) continue; - $markdown = << "$content" -> — [@{$status["account"]["acct"]}]({$status["account"]["url"]}) -MARKDOWN; + if (!is_dir(dirname($fullPath))) mkdir(dirname($fullPath), 0755, true); - if (file_put_contents($path, $markdown) !== false) { - $postsCreated++; + $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'); + $content = trim(preg_replace('/\s+/', ' ', $content)); + + $imageMarkdown = ''; + foreach ($images as $i => $filename) { + $alt = isset($alts[$i]) ? $alts[$i] : ''; + $imageMarkdown .= "\n![" . htmlspecialchars($alt, ENT_QUOTES) . "](" . $filename . ")\n"; + } + + $featuredImage = ''; + if (!empty($images)) { + $featuredImage = "Image: " . $images[0] . "\n"; + } + + $htmlSafeAuthorName = str_replace('@', '@', $origAuthor); + + return << "$content" +> +> — $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/plain"); + header("Content-Type: text/html"); } - echo $message; + echo "

" . htmlspecialchars($message) . "

"; exit; } -} +} \ No newline at end of file