mastodonapi.php aktualisiert
This commit is contained in:
parent
0565c72706
commit
738d5c9900
1 changed files with 169 additions and 65 deletions
232
mastodonapi.php
232
mastodonapi.php
|
|
@ -1,121 +1,225 @@
|
|||
<?php
|
||||
// MastodonAPI extension, https://codeberg.org/simonpipe/yellow-mastodonapi
|
||||
|
||||
class YellowMastodonAPI {
|
||||
const VERSION = "0.1";
|
||||
public $yellow;
|
||||
|
||||
// Initialisierung
|
||||
public function onLoad($yellow) {
|
||||
$this->yellow = $yellow;
|
||||
$yellow->system->setDefault("mastodonapiInstance", "https://mastodon.social");
|
||||
$yellow->system->setDefault("mastodonapiUserId", "");
|
||||
$yellow->system->setDefault("mastodonapiToken", "");
|
||||
$yellow->system->setDefault("mastodonapiHashtag", ""); // Nur Posts mit diesem Hashtag werden importiert
|
||||
$yellow->system->setDefault("mastodonapiTag", "Fediverse"); // Dieser Tag wird im Yellow CMS gesetzt
|
||||
$yellow->system->setDefault("mastodonapiAuthor", "YellowUser");
|
||||
$yellow->system->setDefault("mastodonapiFolder", "content/1-blog/");
|
||||
$yellow->system->setDefault("mastodonapiTriggerPath", "/mastodonapi-webhook-CHANGE_THIS_SECRET_KEY");
|
||||
$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/1-blog/");
|
||||
$this->yellow->system->setDefault("mastodonapiTriggerPath", "/mastodonapi-webhook-CHANGE_THIS_SECRET_KEY");
|
||||
}
|
||||
|
||||
// Webhook-Request
|
||||
public function onRequest($scheme, $address, $base, $location, $fileName) {
|
||||
$triggerPath = $this->yellow->system->get("mastodonapiTriggerPath");
|
||||
if ($location == $triggerPath) {
|
||||
$postsCreated = $this->processMastodonAPI();
|
||||
$postsCreated = $this->processMastodonFeed();
|
||||
if ($postsCreated !== false) {
|
||||
$this->sendResponse("Mastodon API processed. Created $postsCreated posts.", 200);
|
||||
$this->sendResponse("Mastodon API feed processed. Created " . $postsCreated . " posts.", 200);
|
||||
} else {
|
||||
$this->sendResponse("Error processing Mastodon API. Check logs.", 500);
|
||||
$this->sendResponse("Error processing Mastodon API feed. Check logs for details.", 500);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// CLI Command
|
||||
public function onCommand($command, $text) {
|
||||
if ($command == "mastodonapi") {
|
||||
$postsCreated = $this->processMastodonAPI();
|
||||
return $postsCreated !== false ? 0 : 500;
|
||||
}
|
||||
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 processMastodonAPI() {
|
||||
$instance = rtrim($this->yellow->system->get("mastodonapiInstance"), "/");
|
||||
$userId = $this->yellow->system->get("mastodonapiUserId");
|
||||
$token = $this->yellow->system->get("mastodonapiToken");
|
||||
$hashtag = $this->yellow->system->get("mastodonapiHashtag");
|
||||
$tag = $this->yellow->system->get("mastodonapiTag");
|
||||
$author = $this->yellow->system->get("mastodonapiAuthor");
|
||||
$folder = $this->yellow->system->get("mastodonapiFolder");
|
||||
// API-Logik
|
||||
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");
|
||||
|
||||
if (empty($userId) || empty($token)) {
|
||||
error_log("ERROR: MastodonAPI: Missing user ID or token.");
|
||||
if (empty($instanceUrl) || empty($accountId) || empty($accessToken)) {
|
||||
error_log("ERROR: MastodonAPI: Missing configuration values.");
|
||||
return false;
|
||||
}
|
||||
|
||||
$url = "$instance/api/v1/accounts/$userId/statuses?exclude_replies=true&exclude_reblogs=true&limit=5";
|
||||
$opts = [
|
||||
"http" => [
|
||||
"method" => "GET",
|
||||
"header" => "Authorization: Bearer $token\r\nUser-Agent: YellowCMS\r\n"
|
||||
$apiUrl = rtrim($instanceUrl, "/") . "/api/v1/accounts/" . $accountId . "/statuses?limit=20";
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'method' => "GET",
|
||||
'header' => "Authorization: Bearer " . $accessToken . "\r\n" .
|
||||
"User-Agent: YellowMastodonAPI\r\n",
|
||||
'timeout' => 30
|
||||
],
|
||||
'ssl' => [
|
||||
'verify_peer' => true,
|
||||
'verify_peer_name' => true
|
||||
]
|
||||
];
|
||||
$context = stream_context_create($opts);
|
||||
$json = @file_get_contents($url, false, $context);
|
||||
]);
|
||||
|
||||
$json = @file_get_contents($apiUrl, false, $context);
|
||||
if ($json === false) {
|
||||
error_log("ERROR: MastodonAPI: Failed to fetch API data.");
|
||||
error_log("ERROR: MastodonAPI: Could not fetch API data from $apiUrl");
|
||||
return false;
|
||||
}
|
||||
|
||||
$statuses = json_decode($json, true);
|
||||
if (!is_array($statuses)) {
|
||||
error_log("ERROR: MastodonAPI: Invalid JSON response.");
|
||||
$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 ($statuses as $status) {
|
||||
// Nur Posts mit dem gewünschten Hashtag importieren
|
||||
if (!empty($hashtag) && stripos($status["content"], "#$hashtag") === false) {
|
||||
|
||||
foreach ($posts as $post) {
|
||||
$contentHtml = $post['content'];
|
||||
$createdAt = $post['created_at'];
|
||||
$tags = array_column($post['tags'], 'name');
|
||||
|
||||
if (!empty($hashtag) && !in_array($hashtag, array_map('strtolower', $tags))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$content = strip_tags($status["content"]);
|
||||
$createdAt = $status["created_at"];
|
||||
$title = mb_substr(preg_replace("/\\s+/", " ", $content), 0, 50);
|
||||
$slug = strtolower(preg_replace("/[^a-z0-9]+/", "-", $title));
|
||||
$datePrefix = date("Y-m-d", strtotime($createdAt));
|
||||
$fileName = "$datePrefix-$slug.md";
|
||||
$path = $this->yellow->system->get("coreServerDocument") . $folder . $fileName;
|
||||
$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($path)) continue;
|
||||
if (file_exists($fullPath)) continue;
|
||||
|
||||
$markdown = <<<MARKDOWN
|
||||
---
|
||||
Title: $title
|
||||
TitleSlug: $slug
|
||||
Published: $createdAt
|
||||
Author: $author
|
||||
Layout: blog
|
||||
Tag: $tag
|
||||
---
|
||||
> "$content"
|
||||
> — [@{$status["account"]["acct"]}]({$status["account"]["url"]})
|
||||
MARKDOWN;
|
||||
if (!is_dir(dirname($fullPath))) mkdir(dirname($fullPath), 0755, true);
|
||||
|
||||
if (file_put_contents($path, $markdown) !== false) {
|
||||
$postsCreated++;
|
||||
$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]) ? $alts[$i] : '';
|
||||
$imageMarkdown .= "\n\n";
|
||||
}
|
||||
|
||||
$featuredImage = '';
|
||||
if (!empty($images)) {
|
||||
$featuredImage = "Image: " . $images[0] . "\n";
|
||||
}
|
||||
|
||||
$htmlSafeAuthorName = str_replace('@', '@', $origAuthor);
|
||||
|
||||
return <<<MD
|
||||
---
|
||||
Title: $title
|
||||
TitleSlug: $slug
|
||||
Published: $formattedDate $formattedTime
|
||||
Author: $yellowAuthor
|
||||
Layout: blog
|
||||
Tag: $yellowTag
|
||||
$featuredImage---
|
||||
$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/plain");
|
||||
header("Content-Type: text/html");
|
||||
}
|
||||
echo $message;
|
||||
echo "<h1>" . htmlspecialchars($message) . "</h1>";
|
||||
exit;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue