yellow = $yellow; $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/2-blog/"); $this->yellow->system->setDefault("mastodonapiTriggerPath", "/mastodonapi-webhook-CHANGE_THIS_SECRET_KEY"); $this->yellow->system->setDefault("mastodonapiLimit", "20"); $this->yellow->system->setDefault("mastodonapiStatus", "public"); // public; draft = page is not visible, user needs to log in, requires draft extension; unlisted = page is not visible, but can be accessed with the correct link $this->yellow->system->setDefault("mastodonapiMaxAge", "0"); // In Stunden. 0 = alle Toots. } public function onRequest($scheme, $address, $base, $location, $fileName) { $triggerPath = $this->yellow->system->get("mastodonapiTriggerPath"); if ($location == $triggerPath) { $postsCreated = $this->processMastodonFeed(); $this->sendResponse( $postsCreated !== false ? "Mastodon/GoToSocial API feed processed. Created $postsCreated posts." : "Error processing Mastodon/GoToSocial API feed.", $postsCreated !== false ? 200 : 500 ); return 1; } return 0; } 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; } } 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"); $status = $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 = ["User-Agent: YellowMastodonAPI"]; if (!empty($accessToken)) { $headers[] = "Authorization: Bearer " . $accessToken; } $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); curl_close($ch); if ($json === false || $httpCode < 200 || $httpCode >= 300) { return false; } $posts = json_decode($json, true); if (!is_array($posts)) { return false; } $config = $this->getYellowConfig(); $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) { $postTimestamp = strtotime($createdAt); $threshold = time() - ($maxAgeHours * 3600); if ($postTimestamp < $threshold) continue; } $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'] ?: "#"; $imageFilenames = []; $imageAltTexts = []; 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'] ?? ''; } } } $markdownContent = $this->generateMarkdown( $title, $titleSlug, $createdAt, $contentHtml, $authorName, $authorUrl, $defaultAuthor, $defaultTag, $imageFilenames, $imageAltTexts, $status ); file_put_contents($fullPath, $markdownContent); $postsCreated++; } return $postsCreated; } 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)); $fullPath = $dirPath . $fileName; if (file_exists($fullPath)) return $fileName; $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); curl_close($ch); if ($imgData === false || $httpCode < 200 || $httpCode >= 300) { return false; } file_put_contents($fullPath, $imgData); 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 = [], $status = "public") { $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 = ''; if (preg_match('/(\s*(#\w+))+$/u', $content, $matches)) { $content = substr($content, 0, -strlen($matches[0])); if (preg_match_all('/#(\w+)/u', $matches[0], $tags)) { $safeHashtags = array_map(function($tag) { return str_replace('#', '#', $tag); }, $tags[0]); $hashtagString = implode(' ', $safeHashtags); } } $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); $blockquote = "> $content"; if (!empty($hashtagString)) { $blockquote .= "\n>\n> " . $hashtagString; } $blockquote .= "\n>\n> — $htmlSafeAuthorName"; return <<yellow ? $this->yellow->system : null; } private function sendResponse($message, $statusCode = 200) { if (!headers_sent()) { header("HTTP/1.1 $statusCode"); header("Content-Type: text/html; charset=UTF-8"); } // Basis-URL aus der aktuellen Anfrage bestimmen $fullUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}"; // Trigger-Pfad aus der Yellow-Konfiguration holen $triggerPath = $this->yellow->system->get("mastodonapiTriggerPath"); // Ziel-URL: Alles vor dem Trigger-Pfad abschneiden $redirectUrl = str_replace($triggerPath, "/", $fullUrl); echo ""; echo ""; echo "Mastodon/GoToSocial API"; echo "

" . htmlspecialchars($message) . "

"; echo "

You will be redirected in 5 seconds...

"; echo ""; exit; } }