yellow-mastodonapi/mastodonapi.php
2025-08-13 13:36:27 +02:00

258 lines
No EOL
9.4 KiB
PHP

<?php
// MastodonAPI extension, https://codeberg.org/simonpipe/yellow-mastodonapi
class YellowMastodonAPI {
const VERSION = "0.3";
public $yellow;
// Initialisierung
public function onLoad($yellow) {
$this->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("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
}
// 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);
}
return 1;
}
return 0;
}
// CLI Command
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;
}
}
// API-Logik
private function processMastodonFeed() {
$instanceUrl = $this->yellow->system->get("mastodonapiInstanceUrl");
$accountId = $this->yellow->system->get("mastodonapiAccountId");
$accessToken = $this->yellow->system->get("mastodonapiAccessToken"); // optional
$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");
$limit = intval($this->yellow->system->get("mastodonapiLimit"));
if (empty($instanceUrl) || empty($accountId)) {
error_log("ERROR: MastodonAPI: Missing configuration values.");
return false;
}
$apiUrl = rtrim($instanceUrl, "/") . "/api/v1/accounts/" . $accountId . "/statuses?limit=" . $limit;
$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
]
]);
$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");
return false;
}
$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 ($posts as $post) {
// 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');
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'] ?: "#";
// 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]) ? 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);
return <<<MD
---
Title: $title
TitleSlug: $slug
Published: $formattedDate $formattedTime
Author: $yellowAuthor
Layout: blog
Tag: $yellowTag
$featuredImage$featuredImageAlt---
$imageMarkdown
> "$content"
>
> — <a target="_blank" href="$origUrl">$htmlSafeAuthorName</a>
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/html");
}
echo "<h1>" . htmlspecialchars($message) . "</h1>";
exit;
}
}