diff --git a/mastodonapi.php b/mastodonapi.php index 02471a5..82788fb 100644 --- a/mastodonapi.php +++ b/mastodonapi.php @@ -1,40 +1,40 @@ 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("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"); - $this->yellow->system->setDefault("mastodonapiLimit", "20"); // neu: Anzahl Posts + $this->yellow->system->setDefault("mastodonapiLimit", "20"); } - // 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); - } + $this->sendResponse( + $postsCreated !== false + ? "Mastodon/GoToSocial API feed processed. Created $postsCreated posts." + : "Error processing Mastodon/GoToSocial API feed. Check logs for details.", + $postsCreated !== false ? 200 : 500 + ); return 1; } return 0; } - // CLI Command public function onCommand($command, $text) { if (!$this->yellow) return 500; switch ($command) { @@ -47,7 +47,6 @@ class YellowMastodonAPI { } } - // API-Logik private function processMastodonFeed() { $instanceUrl = $this->yellow->system->get("mastodonapiInstanceUrl"); $accountId = $this->yellow->system->get("mastodonapiAccountId"); @@ -65,38 +64,28 @@ class YellowMastodonAPI { $apiUrl = rtrim($instanceUrl, "/") . "/api/v1/accounts/" . $accountId . "/statuses?limit=" . $limit; - $headers = [ - "User-Agent: YellowMastodonAPI" - ]; + $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 - ] + // Daten via cURL abrufen (funktioniert bei Mastodon & GoToSocial) + $ch = curl_init($apiUrl); + curl_setopt_array($ch, [ + 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); + $curlError = curl_error($ch); + curl_close($ch); - $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"); + if ($json === false || $httpCode < 200 || $httpCode >= 300) { + error_log("ERROR: Could not fetch API data from $apiUrl (HTTP $httpCode, Error: $curlError)"); return false; } @@ -110,18 +99,13 @@ class YellowMastodonAPI { $postsCreated = 0; foreach ($posts as $post) { + // Keine Boosts / Antworten + if (!empty($post['reblog']) || (!empty($post['reblogged']) && $post['reblogged'] === true)) continue; + if (!is_null($post['in_reply_to_id'])) continue; - // 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'); + $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; @@ -134,18 +118,17 @@ class YellowMastodonAPI { $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/'); + + foreach ($post['media_attachments'] ?? [] as $media) { + if (($media['type'] ?? '') === 'image') { + $filename = $this->downloadAndSaveImage($media, $config->get("coreServerDocument") . 'media/images/'); if ($filename) { $imageFilenames[] = $filename; $imageAltTexts[] = $media['description'] ?? ''; @@ -173,17 +156,54 @@ class YellowMastodonAPI { return $postsCreated; } - private function downloadAndSaveImage($imageUrl, $dirPath) { + 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)) { + error_log("ERROR: No usable image URL found in media attachment: " . json_encode($media)); + return false; + } + 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); + + $ch = curl_init($imageUrl); + $headers = ["User-Agent: 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 => true, + CURLOPT_SSL_VERIFYHOST => 2, + ]); + $imgData = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlError = curl_error($ch); + curl_close($ch); + + if ($imgData === false || $httpCode < 200 || $httpCode >= 300) { + error_log("ERROR: Could not download image from $imageUrl (HTTP $httpCode, Error: $curlError)"); + return false; + } + + file_put_contents($fullPath, $imgData); return $fileName; } + // Rest der Helper-Methoden wie bei dir (generateTitle, generateTitleSlug, generateMarkdown, getYellowConfig, sendResponse) ... + private function generateTitle($content) { $text = strip_tags($content); $text = preg_replace('/https?:\/\/[^\s]+|#\w+/u', '', $text); @@ -207,50 +227,60 @@ class YellowMastodonAPI { $content = strip_tags($html); $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + // Hashtag-Block extrahieren $hashtagString = ''; if (preg_match('/(\s*(#\w+))+$/u', $content, $matches)) { $content = substr($content, 0, -strlen($matches[0])); if (preg_match_all('/#(\w+)/u', $matches[0], $tags)) { - $hashtagString = "\n> " . implode(' ', $tags[0]); + // Ersetzt '#' durch '#' für die Ausgabe + $safeHashtags = array_map(function($tag) { + return str_replace('#', '#', $tag); + }, $tags[0]); + $hashtagString = implode(' ', $safeHashtags); } } - $content = trim(preg_replace('/\s+/', ' ', $content)); + $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"; + $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); + + // Baut den Blockquote-String robust und schrittweise zusammen + $blockquote = "> \"$content\""; + if (!empty($hashtagString)) { + $blockquote .= "\n>\n> " . $hashtagString; + } + $blockquote .= "\n>\n> — $htmlSafeAuthorName"; + + return << "$content"$hashtagString -> -> — $htmlSafeAuthorName -MD; -} - private function getYellowConfig() { return $this->yellow ? $this->yellow->system : null; }