mastodonapi.php aktualisiert

This commit is contained in:
simonpipe 2025-08-13 16:45:08 +02:00
parent 1db48837f6
commit 0fe5607616

View file

@ -1,40 +1,40 @@
<?php <?php
// MastodonAPI extension, https://codeberg.org/simonpipe/yellow-mastodonapi // MastodonAPI & GoToSocial extension
// Kompatibel mit Mastodon-Servern & GoToSocial-Instanzen
// https://codeberg.org/simonpipe/yellow-mastodonapi (angepasst)
class YellowMastodonAPI { class YellowMastodonAPI {
const VERSION = "0.4"; const VERSION = "0.6";
public $yellow; public $yellow;
// Initialisierung
public function onLoad($yellow) { public function onLoad($yellow) {
$this->yellow = $yellow; $this->yellow = $yellow;
$this->yellow->system->setDefault("mastodonapiInstanceUrl", "https://example.social"); $this->yellow->system->setDefault("mastodonapiInstanceUrl", "https://example.social");
$this->yellow->system->setDefault("mastodonapiAccountId", ""); $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("mastodonapiHashtag", "");
$this->yellow->system->setDefault("mastodonapiAuthor", "YellowUser"); $this->yellow->system->setDefault("mastodonapiAuthor", "YellowUser");
$this->yellow->system->setDefault("mastodonapiTag", "Fediverse"); $this->yellow->system->setDefault("mastodonapiTag", "Fediverse");
$this->yellow->system->setDefault("mastodonapiFolder", "content/1-blog/"); $this->yellow->system->setDefault("mastodonapiFolder", "content/1-blog/");
$this->yellow->system->setDefault("mastodonapiTriggerPath", "/mastodonapi-webhook-CHANGE_THIS_SECRET_KEY"); $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) { public function onRequest($scheme, $address, $base, $location, $fileName) {
$triggerPath = $this->yellow->system->get("mastodonapiTriggerPath"); $triggerPath = $this->yellow->system->get("mastodonapiTriggerPath");
if ($location == $triggerPath) { if ($location == $triggerPath) {
$postsCreated = $this->processMastodonFeed(); $postsCreated = $this->processMastodonFeed();
if ($postsCreated !== false) { $this->sendResponse(
$this->sendResponse("Mastodon API feed processed. Created " . $postsCreated . " posts.", 200); $postsCreated !== false
} else { ? "Mastodon/GoToSocial API feed processed. Created $postsCreated posts."
$this->sendResponse("Error processing Mastodon API feed. Check logs for details.", 500); : "Error processing Mastodon/GoToSocial API feed. Check logs for details.",
} $postsCreated !== false ? 200 : 500
);
return 1; return 1;
} }
return 0; return 0;
} }
// CLI Command
public function onCommand($command, $text) { public function onCommand($command, $text) {
if (!$this->yellow) return 500; if (!$this->yellow) return 500;
switch ($command) { switch ($command) {
@ -47,7 +47,6 @@ class YellowMastodonAPI {
} }
} }
// API-Logik
private function processMastodonFeed() { private function processMastodonFeed() {
$instanceUrl = $this->yellow->system->get("mastodonapiInstanceUrl"); $instanceUrl = $this->yellow->system->get("mastodonapiInstanceUrl");
$accountId = $this->yellow->system->get("mastodonapiAccountId"); $accountId = $this->yellow->system->get("mastodonapiAccountId");
@ -65,38 +64,28 @@ class YellowMastodonAPI {
$apiUrl = rtrim($instanceUrl, "/") . "/api/v1/accounts/" . $accountId . "/statuses?limit=" . $limit; $apiUrl = rtrim($instanceUrl, "/") . "/api/v1/accounts/" . $accountId . "/statuses?limit=" . $limit;
$headers = [ $headers = ["User-Agent: YellowMastodonAPI"];
"User-Agent: YellowMastodonAPI"
];
if (!empty($accessToken)) { if (!empty($accessToken)) {
$headers[] = "Authorization: Bearer " . $accessToken; $headers[] = "Authorization: Bearer " . $accessToken;
} }
$context = stream_context_create([ // Daten via cURL abrufen (funktioniert bei Mastodon & GoToSocial)
'http' => [ $ch = curl_init($apiUrl);
'method' => "GET", curl_setopt_array($ch, [
'header' => implode("\r\n", $headers) . "\r\n", CURLOPT_RETURNTRANSFER => true,
'timeout' => 30 CURLOPT_FOLLOWLOCATION => true,
], CURLOPT_TIMEOUT => 30,
'ssl' => [ CURLOPT_HTTPHEADER => $headers,
'verify_peer' => true, CURLOPT_SSL_VERIFYPEER => true,
'verify_peer_name' => 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); if ($json === false || $httpCode < 200 || $httpCode >= 300) {
error_log("ERROR: Could not fetch API data from $apiUrl (HTTP $httpCode, Error: $curlError)");
// 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");
return false; return false;
} }
@ -110,18 +99,13 @@ class YellowMastodonAPI {
$postsCreated = 0; $postsCreated = 0;
foreach ($posts as $post) { 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 $contentHtml = $post['content'] ?? '';
if (!empty($post['reblog']) || (!empty($post['reblogged']) && $post['reblogged'] === true)) { $createdAt = $post['created_at'] ?? '';
continue; $tags = array_column($post['tags'] ?? [], 'name');
}
if (!is_null($post['in_reply_to_id'])) {
continue;
}
$contentHtml = $post['content'];
$createdAt = $post['created_at'];
$tags = array_column($post['tags'], 'name');
if (!empty($hashtag) && !in_array(strtolower($hashtag), array_map('strtolower', $tags))) { if (!empty($hashtag) && !in_array(strtolower($hashtag), array_map('strtolower', $tags))) {
continue; continue;
@ -134,18 +118,17 @@ class YellowMastodonAPI {
$fullPath = $config->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; $authorName = $post['account']['display_name'] ?: $defaultAuthor;
$authorUrl = $post['account']['url'] ?: "#"; $authorUrl = $post['account']['url'] ?: "#";
// Bilder + Alt-Text
$imageFilenames = []; $imageFilenames = [];
$imageAltTexts = []; $imageAltTexts = [];
foreach ($post['media_attachments'] as $media) {
if ($media['type'] === 'image' && !empty($media['url'])) { foreach ($post['media_attachments'] ?? [] as $media) {
$filename = $this->downloadAndSaveImage($media['url'], $config->get("coreServerDocument") . 'media/images/'); if (($media['type'] ?? '') === 'image') {
$filename = $this->downloadAndSaveImage($media, $config->get("coreServerDocument") . 'media/images/');
if ($filename) { if ($filename) {
$imageFilenames[] = $filename; $imageFilenames[] = $filename;
$imageAltTexts[] = $media['description'] ?? ''; $imageAltTexts[] = $media['description'] ?? '';
@ -173,17 +156,54 @@ class YellowMastodonAPI {
return $postsCreated; 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); if (!is_dir($dirPath)) mkdir($dirPath, 0755, true);
$fileName = basename(parse_url($imageUrl, PHP_URL_PATH)); $fileName = basename(parse_url($imageUrl, PHP_URL_PATH));
$fullPath = $dirPath . $fileName; $fullPath = $dirPath . $fileName;
if (file_exists($fullPath)) return $fileName; if (file_exists($fullPath)) return $fileName;
$img = @file_get_contents($imageUrl);
if ($img === false) return false; $ch = curl_init($imageUrl);
file_put_contents($fullPath, $img); $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; return $fileName;
} }
// Rest der Helper-Methoden wie bei dir (generateTitle, generateTitleSlug, generateMarkdown, getYellowConfig, sendResponse) ...
private function generateTitle($content) { private function generateTitle($content) {
$text = strip_tags($content); $text = strip_tags($content);
$text = preg_replace('/https?:\/\/[^\s]+|#\w+/u', '', $text); $text = preg_replace('/https?:\/\/[^\s]+|#\w+/u', '', $text);
@ -207,11 +227,16 @@ class YellowMastodonAPI {
$content = strip_tags($html); $content = strip_tags($html);
$content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5, 'UTF-8');
// Hashtag-Block extrahieren
$hashtagString = ''; $hashtagString = '';
if (preg_match('/(\s*(#\w+))+$/u', $content, $matches)) { if (preg_match('/(\s*(#\w+))+$/u', $content, $matches)) {
$content = substr($content, 0, -strlen($matches[0])); $content = substr($content, 0, -strlen($matches[0]));
if (preg_match_all('/#(\w+)/u', $matches[0], $tags)) { if (preg_match_all('/#(\w+)/u', $matches[0], $tags)) {
$hashtagString = "\n> " . implode(' ', $tags[0]); // Ersetzt '#' durch '&num;' für die Ausgabe
$safeHashtags = array_map(function($tag) {
return str_replace('#', '&num;', $tag);
}, $tags[0]);
$hashtagString = implode(' ', $safeHashtags);
} }
} }
@ -235,6 +260,13 @@ class YellowMastodonAPI {
$htmlSafeAuthorName = str_replace('@', '&#64;', $origAuthor); $htmlSafeAuthorName = str_replace('@', '&#64;', $origAuthor);
// Baut den Blockquote-String robust und schrittweise zusammen
$blockquote = "> \"$content\"";
if (!empty($hashtagString)) {
$blockquote .= "\n>\n> " . $hashtagString;
}
$blockquote .= "\n>\n> — <a target=\"_blank\" href=\"$origUrl\">$htmlSafeAuthorName</a>";
return <<<MD return <<<MD
--- ---
Title: $title Title: $title
@ -245,9 +277,7 @@ Layout: blog
Tag: $yellowTag Tag: $yellowTag
$featuredImage$featuredImageAlt--- $featuredImage$featuredImageAlt---
$imageMarkdown $imageMarkdown
> "$content"$hashtagString $blockquote
>
> <a target="_blank" href="$origUrl">$htmlSafeAuthorName</a>
MD; MD;
} }