226 lines
No EOL
9.8 KiB
PHP
226 lines
No EOL
9.8 KiB
PHP
<?php
|
|
// Mastodonapi extension, https://codeberg.org/simonpipe/yellow-mastodonapi
|
|
|
|
class YellowMastodonapi {
|
|
const VERSION = "0.9.3";
|
|
public $yellow; // access to API
|
|
|
|
// Initialize extension
|
|
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("mastodonapiUpdateInterval", "30");
|
|
$this->yellow->system->setDefault("mastodonapiLimit", "20");
|
|
$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("mastodonapiMaxAge", "0");
|
|
$this->yellow->system->setDefault("mastodonapiStatus", "public");
|
|
}
|
|
|
|
// Handle update
|
|
public function onUpdate($action) {
|
|
if ( $action=="mastodon" || $action=="daily" || $action=="install" ) {
|
|
$this->updateMastodonPosts();
|
|
}
|
|
}
|
|
|
|
// Handle command
|
|
public function onCommand($command, $text) {
|
|
if ( $command=="mastodonapi" ) {
|
|
echo "Mastodon Import gestartet...\n";
|
|
$count = $this->updateMastodonPosts();
|
|
if ( $count===false ) {
|
|
echo "Fehler: API nicht erreichbar oder falsche ID.\n";
|
|
return 500;
|
|
}
|
|
echo "Erfolg: " . $count . " neue Posts verarbeitet.\n";
|
|
return true;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Handle periodic update
|
|
public function onTick() {
|
|
$interval = intval($this->yellow->system->get("mastodonapiUpdateInterval")) * 60;
|
|
$statusFile = $this->getStatusFile();
|
|
$lastUpdate = file_exists($statusFile) ? intval(file_get_contents($statusFile)) : 0;
|
|
if ( time() - $lastUpdate >= $interval ) {
|
|
$this->updateMastodonPosts();
|
|
}
|
|
}
|
|
|
|
// Update Mastodon posts
|
|
public function updateMastodonPosts() {
|
|
$postsCreated = $this->processMastodonFeed();
|
|
if ( $postsCreated!==false ) {
|
|
$statusFile = $this->getStatusFile();
|
|
file_put_contents($statusFile, time());
|
|
}
|
|
return $postsCreated;
|
|
}
|
|
|
|
// Process Mastodon feed and create pages
|
|
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");
|
|
$statusSetting = $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 = array("User-Agent: YellowMastodonapi/" . self::VERSION);
|
|
if ( !empty($accessToken) ) $headers[] = "Authorization: Bearer " . $accessToken;
|
|
$ch = curl_init($apiUrl);
|
|
curl_setopt_array($ch, array(
|
|
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;
|
|
$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 ) {
|
|
if ( strtotime($createdAt) < ( time() - ( $maxAgeHours * 3600 ) ) ) continue;
|
|
}
|
|
$tags = array_column($post["tags"] ?? array(), "name");
|
|
if ( !empty($hashtag) && !in_array(strtolower($hashtag), array_map("strtolower", $tags)) ) continue;
|
|
$title = $this->generateTitle($contentHtml);
|
|
$titleSlug = $this->generateTitleSlug($title);
|
|
$fileName = date("Y-m-d", strtotime($createdAt)) . "-" . $titleSlug . ".md";
|
|
$fullPath = $this->yellow->system->get("coreServerDocument") . $targetFolder . $fileName;
|
|
if ( file_exists($fullPath) ) continue;
|
|
|
|
if ( !is_dir(dirname($fullPath)) ) {
|
|
mkdir(dirname($fullPath), 0777, true);
|
|
chmod(dirname($fullPath), 0777);
|
|
}
|
|
|
|
$imageFilenames = array();
|
|
$imageAltTexts = array();
|
|
foreach ( $post["media_attachments"] ?? array() as $media ) {
|
|
if ( ( $media["type"] ?? "" )==="image" ) {
|
|
$filename = $this->downloadAndSaveImage($media, $this->yellow->system->get("coreServerDocument") . "media/images/");
|
|
if ( $filename ) {
|
|
$imageFilenames[] = $filename;
|
|
$imageAltTexts[] = $media["description"] ?? "";
|
|
}
|
|
}
|
|
}
|
|
$markdownContent = $this->generateMarkdown(
|
|
$title, $titleSlug, $createdAt, $contentHtml,
|
|
$post["account"]["display_name"] ?: $defaultAuthor,
|
|
$post["account"]["url"] ?: "#",
|
|
$defaultAuthor, $defaultTag, $imageFilenames, $imageAltTexts, $statusSetting
|
|
);
|
|
file_put_contents($fullPath, $markdownContent);
|
|
chmod($fullPath, 0666);
|
|
$postsCreated++;
|
|
}
|
|
return $postsCreated;
|
|
}
|
|
|
|
// Download and save image
|
|
private function downloadAndSaveImage($media, $dirPath) {
|
|
$imageUrl = $media["remote_url"] ?? $media["url"] ?? $media["preview_url"] ?? null;
|
|
if ( empty($imageUrl) ) return false;
|
|
|
|
if ( !is_dir($dirPath) ) {
|
|
mkdir($dirPath, 0777, true);
|
|
chmod($dirPath, 0777);
|
|
}
|
|
|
|
$fileName = basename(parse_url($imageUrl, PHP_URL_PATH));
|
|
$fullPath = $dirPath . $fileName;
|
|
|
|
if ( file_exists($fullPath) ) return $fileName;
|
|
|
|
$ch = curl_init($imageUrl);
|
|
curl_setopt_array($ch, array(
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_FOLLOWLOCATION => true,
|
|
CURLOPT_TIMEOUT => 30,
|
|
CURLOPT_USERAGENT => "YellowMastodonapi/" . self::VERSION,
|
|
CURLOPT_SSL_VERIFYPEER => true,
|
|
));
|
|
$imgData = curl_exec($ch);
|
|
curl_close($ch);
|
|
|
|
if ( !$imgData ) return false;
|
|
file_put_contents($fullPath, $imgData);
|
|
chmod($fullPath, 0666);
|
|
return $fileName;
|
|
}
|
|
|
|
// Generate title from content
|
|
private function generateTitle($content) {
|
|
$text = trim(strip_tags($content));
|
|
$text = preg_replace("/https?:\/\/[^\s]+|#\w+/u", "", $text);
|
|
$words = explode(" ", $text);
|
|
return trim(mb_substr(implode(" ", array_slice($words, 0, 7)), 0, 100, "UTF-8")) ?: "Mastodon Post";
|
|
}
|
|
|
|
// Generate slug from title
|
|
private function generateTitleSlug($title) {
|
|
$slug = mb_strtolower($title, "UTF-8");
|
|
$slug = str_replace(array("ä","ö","ü","ß"," "), array("ae","oe","ue","ss","-"), $slug);
|
|
return trim(preg_replace("/[^a-z0-9-]+/", "", $slug), "-");
|
|
}
|
|
|
|
// Generate markdown content
|
|
private function generateMarkdown($title, $slug, $dateTime, $html, $origAuthor, $origUrl, $yellowAuthor, $yellowTag, $images, $alts, $status) {
|
|
$date = new DateTime($dateTime);
|
|
$pub = $date->format("Y-m-d H:i:s");
|
|
$content = trim(html_entity_decode(strip_tags(str_replace(array("<br>", "</p>"), array("\n", "</p>\n"), $html)), ENT_QUOTES | ENT_HTML5, "UTF-8"));
|
|
$content = str_replace(array("\n", "#"), array("\n> ", "#"), $content);
|
|
$imageMarkdown = "";
|
|
$featuredImage = $featuredImageAlt = "";
|
|
foreach ( $images as $i => $filename ) {
|
|
$alt = isset($alts[$i]) ? htmlspecialchars($alts[$i], ENT_QUOTES) : "";
|
|
$imageMarkdown .= "\n<img src=\"/media/images/$filename\" alt=\"$alt\"/>\n";
|
|
if ( $i===0 ) {
|
|
$featuredImage = "Image: $filename\n";
|
|
$featuredImageAlt = "ImageAlt: $alt\n";
|
|
}
|
|
}
|
|
$authorSafe = str_replace("@", "@", $origAuthor);
|
|
return "---\n" .
|
|
"Title: $title\n" .
|
|
"TitleSlug: $slug\n" .
|
|
"Published: $pub\n" .
|
|
"Author: $yellowAuthor\n" .
|
|
"Layout: blog\n" .
|
|
"Tag: $yellowTag\n" .
|
|
"Status: $status\n" .
|
|
$featuredImage . $featuredImageAlt .
|
|
"---\n\n" .
|
|
$imageMarkdown . "\n" .
|
|
"> " . $content . "\n>\n" .
|
|
"> — <a target=\"_blank\" href=\"$origUrl\">$authorSafe</a>";
|
|
}
|
|
|
|
// Return status file path
|
|
private function getStatusFile() {
|
|
return $this->yellow->system->get("coreExtensionsDirectory") . "mastodonapi.log";
|
|
}
|
|
}
|