Update mastodonapi.php

This commit is contained in:
simonpipe 2026-01-07 09:10:22 +01:00
parent 315da16328
commit 925f996db3

View file

@ -16,20 +16,30 @@ class YellowMastodonapi {
$this->yellow->system->setDefault("mastodonapiTag", "Fediverse"); $this->yellow->system->setDefault("mastodonapiTag", "Fediverse");
$this->yellow->system->setDefault("mastodonapiFolder", "content/2-blog/"); $this->yellow->system->setDefault("mastodonapiFolder", "content/2-blog/");
$this->yellow->system->setDefault("mastodonapiLimit", "20"); $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("mastodonapiStatus", "public");
$this->yellow->system->setDefault("mastodonapiMaxAge", "0"); // In Stunden. 0 = alle Toots. $this->yellow->system->setDefault("mastodonapiMaxAge", "0");
$this->yellow->system->setDefault("mastodonapiUpdateInterval", "30"); $this->yellow->system->setDefault("mastodonapiUpdateInterval", "30");
$this->yellow->system->setDefault("mastodonapiLastUpdate", "0"); $this->yellow->system->setDefault("mastodonapiLastUpdate", "0");
} }
// Reagiert auf 'php yellow.php mastodon' oder das System-Event 'daily' // Reagiert auf System-Events und manuelle Aufrufe
public function onUpdate($action) { public function onUpdate($action) {
if ($action == "mastodon" || $action == "daily" || $action == "clean") { if ($action == "mastodon" || $action == "daily" || $action == "install") {
$this->updateMastodonPosts(); $this->updateMastodonPosts();
} }
} }
// Prüft bei Seitenaufrufen, ob das Intervall abgelaufen ist // Ermöglicht 'php yellow.php mastodonapi' im Terminal
public function onCommand($command, $text) {
if ($command == "mastodonapi") {
// Wir leiten den Befehl einfach an onUpdate weiter
$this->onUpdate("mastodon");
return 0; // Erfolg
}
return 0;
}
// Der "Lazy Cron" für Nutzer ohne echten Cronjob
public function onTick() { public function onTick() {
$interval = intval($this->yellow->system->get("mastodonapiUpdateInterval")) * 60; $interval = intval($this->yellow->system->get("mastodonapiUpdateInterval")) * 60;
$lastUpdate = intval($this->yellow->system->get("mastodonapiLastUpdate")); $lastUpdate = intval($this->yellow->system->get("mastodonapiLastUpdate"));
@ -40,26 +50,11 @@ class YellowMastodonapi {
} }
public function updateMastodonPosts() { public function updateMastodonPosts() {
$instance = $this->yellow->system->get("mastodonapiInstance"); $success = $this->processMastodonFeed();
$userId = $this->yellow->system->get("mastodonapiUserId");
// Dein bestehender Code zum Abrufen der Mastodon-Daten... if ($success !== false) {
// Beispiel: $response = $this->yellow->toolbox->getHttpClient()->getContent($url); $this->yellow->system->set("mastodonapiLastUpdate", time());
$this->yellow->system->save();
// Am Ende den Zeitstempel speichern, damit der nächste Tick bescheid weiß
$this->yellow->system->set("mastodonapiLastUpdate", time());
$this->yellow->system->save();
}
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;
} }
} }
@ -75,16 +70,11 @@ class YellowMastodonapi {
$maxAgeHours = intval($this->yellow->system->get("mastodonapiMaxAge")); $maxAgeHours = intval($this->yellow->system->get("mastodonapiMaxAge"));
$limit = intval($this->yellow->system->get("mastodonapiLimit")); $limit = intval($this->yellow->system->get("mastodonapiLimit"));
if (empty($instanceUrl) || empty($accountId)) { if (empty($instanceUrl) || empty($accountId)) return false;
return false;
}
$apiUrl = rtrim($instanceUrl, "/") . "/api/v1/accounts/" . $accountId . "/statuses?limit=" . $limit; $apiUrl = rtrim($instanceUrl, "/") . "/api/v1/accounts/" . $accountId . "/statuses?limit=" . $limit;
$headers = ["User-Agent: YellowMastodonAPI"]; $headers = ["User-Agent: YellowMastodonAPI"];
if (!empty($accessToken)) { if (!empty($accessToken)) $headers[] = "Authorization: Bearer " . $accessToken;
$headers[] = "Authorization: Bearer " . $accessToken;
}
$ch = curl_init($apiUrl); $ch = curl_init($apiUrl);
curl_setopt_array($ch, [ curl_setopt_array($ch, [
@ -99,18 +89,12 @@ class YellowMastodonapi {
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch); curl_close($ch);
if ($json === false || $httpCode < 200 || $httpCode >= 300) { if ($json === false || $httpCode < 200 || $httpCode >= 300) return false;
return false;
}
$posts = json_decode($json, true); $posts = json_decode($json, true);
if (!is_array($posts)) { if (!is_array($posts)) return false;
return false;
}
$config = $this->getYellowConfig();
$postsCreated = 0; $postsCreated = 0;
foreach ($posts as $post) { foreach ($posts as $post) {
if (!empty($post['reblog']) || (!empty($post['reblogged']) && $post['reblogged'] === true)) continue; if (!empty($post['reblog']) || (!empty($post['reblogged']) && $post['reblogged'] === true)) continue;
if (!is_null($post['in_reply_to_id'])) continue; if (!is_null($post['in_reply_to_id'])) continue;
@ -118,34 +102,25 @@ class YellowMastodonapi {
$contentHtml = $post['content'] ?? ''; $contentHtml = $post['content'] ?? '';
$createdAt = $post['created_at'] ?? ''; $createdAt = $post['created_at'] ?? '';
if ($maxAgeHours > 0) { if ($maxAgeHours > 0) {
$postTimestamp = strtotime($createdAt); if (strtotime($createdAt) < (time() - ($maxAgeHours * 3600))) continue;
$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))) { $tags = array_column($post['tags'] ?? [], 'name');
continue; if (!empty($hashtag) && !in_array(strtolower($hashtag), array_map('strtolower', $tags))) continue;
}
$title = $this->generateTitle($contentHtml); $title = $this->generateTitle($contentHtml);
$titleSlug = $this->generateTitleSlug($title); $titleSlug = $this->generateTitleSlug($title);
$datePrefix = date('Y-m-d', strtotime($createdAt)); $fileName = date('Y-m-d', strtotime($createdAt)) . '-' . $titleSlug . '.md';
$fileName = $datePrefix . '-' . $titleSlug . '.md'; $fullPath = $this->yellow->system->get("coreServerDocument") . $targetFolder . $fileName;
$fullPath = $config->get("coreServerDocument") . $targetFolder . $fileName;
if (file_exists($fullPath)) continue; if (file_exists($fullPath)) continue;
if (!is_dir(dirname($fullPath))) mkdir(dirname($fullPath), 0755, true); if (!is_dir(dirname($fullPath))) mkdir(dirname($fullPath), 0755, true);
$authorName = $post['account']['display_name'] ?: $defaultAuthor;
$authorUrl = $post['account']['url'] ?: "#";
$imageFilenames = []; $imageFilenames = [];
$imageAltTexts = []; $imageAltTexts = [];
foreach ($post['media_attachments'] ?? [] as $media) { foreach ($post['media_attachments'] ?? [] as $media) {
if (($media['type'] ?? '') === 'image') { if (($media['type'] ?? '') === 'image') {
$filename = $this->downloadAndSaveImage($media, $config->get("coreServerDocument") . 'media/images/'); $filename = $this->downloadAndSaveImage($media, $this->yellow->system->get("coreServerDocument") . 'media/images/');
if ($filename) { if ($filename) {
$imageFilenames[] = $filename; $imageFilenames[] = $filename;
$imageAltTexts[] = $media['description'] ?? ''; $imageAltTexts[] = $media['description'] ?? '';
@ -154,37 +129,21 @@ class YellowMastodonapi {
} }
$markdownContent = $this->generateMarkdown( $markdownContent = $this->generateMarkdown(
$title, $title, $titleSlug, $createdAt, $contentHtml,
$titleSlug, $post['account']['display_name'] ?: $defaultAuthor,
$createdAt, $post['account']['url'] ?: "#",
$contentHtml, $defaultAuthor, $defaultTag, $imageFilenames, $imageAltTexts, $status
$authorName,
$authorUrl,
$defaultAuthor,
$defaultTag,
$imageFilenames,
$imageAltTexts,
$status
); );
file_put_contents($fullPath, $markdownContent); file_put_contents($fullPath, $markdownContent);
$postsCreated++; $postsCreated++;
} }
return $postsCreated; return $postsCreated;
} }
private function downloadAndSaveImage($media, $dirPath) { private function downloadAndSaveImage($media, $dirPath) {
$imageUrl = $media['remote_url'] $imageUrl = $media['remote_url'] ?? $media['url'] ?? $media['preview_url'] ?? null;
?? $media['url'] if (empty($imageUrl)) return false;
?? $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); if (!is_dir($dirPath)) mkdir($dirPath, 0755, true);
$fileName = basename(parse_url($imageUrl, PHP_URL_PATH)); $fileName = basename(parse_url($imageUrl, PHP_URL_PATH));
@ -192,117 +151,68 @@ class YellowMastodonapi {
if (file_exists($fullPath)) return $fileName; if (file_exists($fullPath)) return $fileName;
$ch = curl_init($imageUrl); $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, [ curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true, CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 30, CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => $headers, CURLOPT_USERAGENT => "YellowMastodonAPI",
CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
]); ]);
$imgData = curl_exec($ch); $imgData = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch); curl_close($ch);
if ($imgData === false || $httpCode < 200 || $httpCode >= 300) { if (!$imgData) return false;
return false;
}
file_put_contents($fullPath, $imgData); file_put_contents($fullPath, $imgData);
return $fileName; return $fileName;
} }
private function generateTitle($content) { private function generateTitle($content) {
$text = strip_tags($content); $text = trim(strip_tags($content));
$text = preg_replace('/https?:\/\/[^\s]+|#\w+/u', '', $text); $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); $words = explode(' ', $text);
return trim(mb_substr(implode(' ', array_slice($words, 0, 7)), 0, 100, 'UTF-8')) ?: "Mastodon Post"; return trim(mb_substr(implode(' ', array_slice($words, 0, 7)), 0, 100, 'UTF-8')) ?: "Mastodon Post";
} }
private function generateTitleSlug($title) { private function generateTitleSlug($title) {
$slug = mb_strtolower($title, 'UTF-8'); $slug = mb_strtolower($title, 'UTF-8');
$slug = str_replace(['ä','ö','ü','Ä','Ö','Ü','ß',' '], ['ae','oe','ue','ae','oe','ue','ss','-'], $slug); $slug = str_replace(['ä','ö','ü','ß',' '], ['ae','oe','ue','ss','-'], $slug);
return trim(preg_replace('/[^a-z0-9-]+/', '', $slug), '-'); return trim(preg_replace('/[^a-z0-9-]+/', '', $slug), '-');
} }
private function generateMarkdown($title, $slug, $dateTime, $html, $origAuthor, $origUrl, $yellowAuthor, $yellowTag, $images = [], $alts = [], $status = "public") { private function generateMarkdown($title, $slug, $dateTime, $html, $origAuthor, $origUrl, $yellowAuthor, $yellowTag, $images, $alts, $status) {
$date = new DateTime($dateTime); $date = new DateTime($dateTime);
$formattedDate = $date->format('Y-m-d'); $pub = $date->format('Y-m-d H:i:s');
$formattedTime = $date->format('H:i:s'); $content = trim(html_entity_decode(strip_tags(str_replace(["<br>", "</p>"], ["\n", "</p>\n"], $html)), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
$content = str_replace("\n", "\n> ", $content);
// 1. Schritt: HTML-Umbrüche in Newlines umwandeln $imageMarkdown = '';
$content = str_replace(["<br>", "<br />", "<br/>", "</p>"], ["\n", "\n", "\n", "</p>\n"], $html); $featuredImage = $featuredImageAlt = '';
foreach ($images as $i => $filename) {
// 2. Schritt: Tags entfernen $alt = isset($alts[$i]) ? htmlspecialchars($alts[$i], ENT_QUOTES) : '';
$content = strip_tags($content); $imageMarkdown .= "\n<img src=\"/media/images/$filename\" alt=\"$alt\"/>\n";
if ($i === 0) {
// 3. Schritt: HTML-Entities dekodieren $featuredImage = "Image: $filename\n";
$content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $featuredImageAlt = "ImageAlt: $alt\n";
}
$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('#', '&num;', $tag);
}, $tags[0]);
$hashtagString = implode(' ', $safeHashtags);
} }
}
// WICHTIG: Nur mehrfache LEERZEICHEN kürzen, aber NEWLINES (\n) behalten! $authorSafe = str_replace('@', '&#64;', $origAuthor);
$content = trim(preg_replace('/[ \t]+/', ' ', $content));
// Jetzt die Zeilenumbrüche für das Zitat-Layout vorbereiten return <<<MD
$content = str_replace("\n", "\n> ", $content);
$imageMarkdown = '';
foreach ($images as $i => $filename) {
$alt = isset($alts[$i]) ? htmlspecialchars($alts[$i], ENT_QUOTES) : '';
$imageMarkdown .= "\n<img src=\"/media/images/" . $filename . "\" alt=\"" . $alt . "\"/>\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('@', '&#64;', $origAuthor);
// Blockquote final zusammenbauen
$blockquote = "> $content";
if (!empty($hashtagString)) {
$blockquote .= "\n>\n> " . $hashtagString;
}
$blockquote .= "\n>\n> — <a target=\"_blank\" href=\"$origUrl\">$htmlSafeAuthorName</a>";
return <<<MD
--- ---
Title: $title Title: $title
TitleSlug: $slug TitleSlug: $slug
Published: $formattedDate $formattedTime Published: $pub
Author: $yellowAuthor Author: $yellowAuthor
Layout: blog Layout: blog
Tag: $yellowTag Tag: $yellowTag
Status: $status Status: $status
$featuredImage$featuredImageAlt--- {$featuredImage}{$featuredImageAlt}---
$imageMarkdown $imageMarkdown
$blockquote > $content
>
> <a target="_blank" href="$origUrl">$authorSafe</a>
MD; MD;
} }
private function getYellowConfig() {
return $this->yellow ? $this->yellow->system : null;
}
} }