308 lines
No EOL
12 KiB
PHP
308 lines
No EOL
12 KiB
PHP
<?php
|
|
// Public Domain
|
|
// MastodonAPI extension, https://codeberg.org/simonpipe/yellow-mastodonapi
|
|
|
|
class YellowMastodonapi {
|
|
const VERSION = "0.3.0";
|
|
public $yellow;
|
|
|
|
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", "");
|
|
$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("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.
|
|
$this->yellow->system->setDefault("mastodonapiUpdateInterval", "30");
|
|
$this->yellow->system->setDefault("mastodonapiLastUpdate", "0");
|
|
}
|
|
|
|
// Reagiert auf 'php yellow.php mastodon' oder das System-Event 'daily'
|
|
public function onUpdate($action) {
|
|
if ($action == "mastodon" || $action == "daily" || $action == "clean") {
|
|
$this->updateMastodonPosts();
|
|
}
|
|
}
|
|
|
|
// Prüft bei Seitenaufrufen, ob das Intervall abgelaufen ist
|
|
public function onTick() {
|
|
$interval = intval($this->yellow->system->get("mastodonapiUpdateInterval")) * 60;
|
|
$lastUpdate = intval($this->yellow->system->get("mastodonapiLastUpdate"));
|
|
|
|
if (time() - $lastUpdate >= $interval) {
|
|
$this->updateMastodonPosts();
|
|
}
|
|
}
|
|
|
|
public function updateMastodonPosts() {
|
|
$instance = $this->yellow->system->get("mastodonapiInstance");
|
|
$userId = $this->yellow->system->get("mastodonapiUserId");
|
|
|
|
// Dein bestehender Code zum Abrufen der Mastodon-Daten...
|
|
// Beispiel: $response = $this->yellow->toolbox->getHttpClient()->getContent($url);
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
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');
|
|
|
|
// 1. Schritt: HTML-Umbrüche in Newlines umwandeln
|
|
$content = str_replace(["<br>", "<br />", "<br/>", "</p>"], ["\n", "\n", "\n", "</p>\n"], $html);
|
|
|
|
// 2. Schritt: Tags entfernen
|
|
$content = strip_tags($content);
|
|
|
|
// 3. Schritt: HTML-Entities dekodieren
|
|
$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);
|
|
}
|
|
}
|
|
|
|
// WICHTIG: Nur mehrfache LEERZEICHEN kürzen, aber NEWLINES (\n) behalten!
|
|
$content = trim(preg_replace('/[ \t]+/', ' ', $content));
|
|
|
|
// Jetzt die Zeilenumbrüche für das Zitat-Layout vorbereiten
|
|
$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('@', '@', $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
|
|
TitleSlug: $slug
|
|
Published: $formattedDate $formattedTime
|
|
Author: $yellowAuthor
|
|
Layout: blog
|
|
Tag: $yellowTag
|
|
Status: $status
|
|
$featuredImage$featuredImageAlt---
|
|
$imageMarkdown
|
|
$blockquote
|
|
MD;
|
|
}
|
|
|
|
private function getYellowConfig() {
|
|
return $this->yellow ? $this->yellow->system : null;
|
|
}
|
|
}
|