Compare commits

..

No commits in common. "main" and "0.1" have entirely different histories.
main ... 0.1

10 changed files with 303 additions and 357 deletions

6
.gitattributes vendored
View file

@ -1,6 +0,0 @@
.gitattributes export-ignore
LICENSE export-ignore
help*.md export-ignore
readme*.md export-ignore
# Auto detect text files and perform LF normalization
* text=auto

View file

@ -1,5 +1,3 @@
UNLICENSE
This is free and unencumbered software released into the public domain. This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or Anyone is free to copy, modify, publish, use, compile, sell, or

View file

@ -1,14 +1,13 @@
# Datenstrom Yellow extension settings # Datenstrom Yellow extension settings
Extension: Mastodonapi Extension: MastodonAPI
Version: 0.9.9 Version: 0.1
Description: Import your toots into your blog. Description: Mastodon-API Feed to Blog Post (For Mastodon & GoToSocial)
Developer: Simon Rohr Developer: Simon Pipe
Tag: feature Tag: feature
DownloadUrl: https://git.thepipes.ch/simon/yellow-mastodonapi/archive/main.zip Download: https://codeberg.org/simonpipe/yellow-mastodonapi/archive/main.zip
DocumentationUrl: https://git.thepipes.ch/simon/yellow-mastodonapi Documentation: https://codeberg.org/simonpipe/yellow-mastodonapi
DocumentationLanguage: en, de DocumentationLanguage: en, de
Published: 2026-07-28 15:41:00 Published: 2025-08-13 20:52:00
Status: experimental Status: experimental
system/workers/mastodonapi.php: mastodonapi.php, create, update system/workers/mastodonapi.php: mastodonapi.php, create update
system/workers/mastodonapi.status: mastodonapi.status, create, update, careful

View file

@ -1,18 +1,6 @@
# Hilfe für Snac und GoToSocial # Hilfe für GoToSocial
## Snac ## 1. Account-ID herausfinden
### Token erstellen
1. Ersetze in diesem Link die Platzhalter und öffne ihn im Browser:
```
https://DEINE_INSTANZ_URL/oauth/x-snac-get-token
```
→ Anmelden, bestätigen und den **Code** kopieren.
## GoToSocial
### Account-ID herausfinden
1. Öffne dein Profil: 1. Öffne dein Profil:
`https://DEINE_INSTANZ_URL/@DEIN_BENUTZERNAME` `https://DEINE_INSTANZ_URL/@DEIN_BENUTZERNAME`
2. Zeige den **Quelltext** der Seite an (Rechtsklick → *Seitenquelltext anzeigen*). 2. Zeige den **Quelltext** der Seite an (Rechtsklick → *Seitenquelltext anzeigen*).
@ -25,7 +13,7 @@
5. Trage die Account-ID in Yellow in der `yellow-system.ini` unter 5. Trage die Account-ID in Yellow in der `yellow-system.ini` unter
`MastodonapiAccountId` ein. `MastodonapiAccountId` ein.
### Token erstellen ## 2. Token erstellen
1. Melde dich in deiner GoToSocial-Instanz an: 1. Melde dich in deiner GoToSocial-Instanz an:
`https://DEINE_INSTANZ_URL/settings` `https://DEINE_INSTANZ_URL/settings`
@ -41,11 +29,11 @@
→ Anmelden, bestätigen und den **Code** kopieren. → Anmelden, bestätigen und den **Code** kopieren.
6. Ersetze in diesem `curl`-Befehl die Platzhalter: 6. Ersetze in diesem `curl`-Befehl die Platzhalter:
```bash ```bash
curl -X POST https://DEINE_INSTANZ_URL/oauth/token \ curl -X POST https://DEINE_INSTANZ_URL/oauth/token
-d "client_id=DEIN_CLIENT_ID" \ -d "client_id=DEIN_CLIENT_ID"
-d "client_secret=DEIN_CLIENT_SECRET" \ -d "client_secret=DEIN_CLIENT_SECRET"
-d "grant_type=authorization_code" \ -d "grant_type=authorization_code"
-d "code=DER_CODE_AUS_SCHRITT_5" \ -d "code=DER_CODE_AUS_SCHRITT_5"
-d "redirect_uri=urn:ietf:wg:oauth:2.0:oob" -d "redirect_uri=urn:ietf:wg:oauth:2.0:oob"
``` ```
7. Führe den Befehl im **Terminal** aus. 7. Führe den Befehl im **Terminal** aus.

View file

@ -1,18 +1,6 @@
# Help for Snac and GoToSocial # Help for GoToSocial
## Snac ## Find your Account ID
### Create a Token
1. Adjust the following link and paste it into your browser:
```
https://YOUR_INSTANCE_URL/oauth/x-snac-get-token
```
→ Log in, authorize, and copy the **code**.
## GoToSocial
### Find your Account ID
1. Open your profile (`https://YOUR_INSTANCE_URL/@YOUR_USERNAME`) 1. Open your profile (`https://YOUR_INSTANCE_URL/@YOUR_USERNAME`)
2. View the **page source** and search for this line in the `<head>` section: 2. View the **page source** and search for this line in the `<head>` section:
```html ```html
@ -21,7 +9,7 @@
3. You will find your **account ID** in this line between `fileserver` and `attachment`: `.../fileserver/ACCOUNT_ID/attachment/...` 3. You will find your **account ID** in this line between `fileserver` and `attachment`: `.../fileserver/ACCOUNT_ID/attachment/...`
4. Copy your account ID and enter it in Yellow's `yellow-system.ini` under *MastodonapiAccountId*. 4. Copy your account ID and enter it in Yellow's `yellow-system.ini` under *MastodonapiAccountId*.
### Create a Token ## Create a Token
1. Log in to your GoToSocial instance under `/settings` and click **New Application** under **Applications**. 1. Log in to your GoToSocial instance under `/settings` and click **New Application** under **Applications**.
2. Under `Application name (required)`, enter a name (e.g., *yellow-mastodonapi*). The other fields can be left blank. Click **Create**. 2. Under `Application name (required)`, enter a name (e.g., *yellow-mastodonapi*). The other fields can be left blank. Click **Create**.
@ -33,11 +21,11 @@
→ Log in, authorize, and copy the **code**. → Log in, authorize, and copy the **code**.
5. Adjust the following lines: 5. Adjust the following lines:
```bash ```bash
curl -X POST https://DEINE_INSTANZ_URL/oauth/token \ curl -X POST https://DEINE_INSTANZ_URL/oauth/token
-d "client_id=DEIN_CLIENT_ID" \ -d "client_id=DEIN_CLIENT_ID"
-d "client_secret=DEIN_CLIENT_SECRET" \ -d "client_secret=DEIN_CLIENT_SECRET"
-d "grant_type=authorization_code" \ -d "grant_type=authorization_code"
-d "code=DER_CODE_AUS_SCHRITT_4" \ -d "code=DER_CODE_AUS_SCHRITT_5"
-d "redirect_uri=urn:ietf:wg:oauth:2.0:oob" -d "redirect_uri=urn:ietf:wg:oauth:2.0:oob"
``` ```
6. Copy this code into your **terminal** and press Enter. You will now receive your **user token**, which you can enter in Yellow's `yellow-system.ini`. 6. Copy this code into your **terminal** and press Enter. You will now receive your **user token**, which you can enter in Yellow's `yellow-system.ini`.

View file

@ -1,197 +1,154 @@
<?php <?php
// Mastodonapi extension, https://git.thepipes.ch/simon/yellow-mastodonapi // Public Domain
// MastodonAPI extension, https://codeberg.org/simonpipe/yellow-mastodonapi
class YellowMastodonapi { class YellowMastodonAPI {
const VERSION = "0.9.9"; const VERSION = "0.1";
public $yellow; // access to API public $yellow;
// Initialize extension
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", ""); $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("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/2-blog/"); $this->yellow->system->setDefault("mastodonapiFolder", "content/1-blog/");
$this->yellow->system->setDefault("mastodonapiMaxAge", "0"); $this->yellow->system->setDefault("mastodonapiTriggerPath", "/mastodonapi-webhook-CHANGE_THIS_SECRET_KEY");
$this->yellow->system->setDefault("mastodonapiStatus", "public"); $this->yellow->system->setDefault("mastodonapiLimit", "20");
} }
// Handle update public function onRequest($scheme, $address, $base, $location, $fileName) {
public function onUpdate($action) { $triggerPath = $this->yellow->system->get("mastodonapiTriggerPath");
if ( $action=="mastodon" || $action=="daily" || $action=="install" ) { if ($location == $triggerPath) {
$this->updateMastodonPosts(); $postsCreated = $this->processMastodonFeed();
} $this->sendResponse(
} $postsCreated !== false
? "Mastodon/GoToSocial API feed processed. Created $postsCreated posts."
// Handle command : "Error processing Mastodon/GoToSocial API feed.",
public function onCommand($command, $text) { $postsCreated !== false ? 200 : 500
if ( $command=="mastodonapi" ) { );
echo "Mastodon Import gestartet...\n"; return 1;
$count = $this->updateMastodonPosts();
if ( $count===false ) {
echo "Fehler: API nicht erreichbar, falsche ID oder Zugriff verweigert.\n";
return 500;
}
echo "Erfolg: " . $count . " neue Posts verarbeitet.\n";
return true;
} }
return 0; return 0;
} }
// Handle periodic update public function onCommand($command, $text) {
public function onTick() { if (!$this->yellow) return 500;
$interval = intval($this->yellow->system->get("mastodonapiUpdateInterval")) * 60; switch ($command) {
$statusFile = $this->getStatusFile(); case "mastodonapi":
$lastUpdate = file_exists($statusFile) ? intval(file_get_contents($statusFile)) : 0; if (!empty($text)) return 400;
if ( time() - $lastUpdate >= $interval ) { $postsCreated = $this->processMastodonFeed();
$this->updateMastodonPosts(); return ($postsCreated !== false) ? 0 : 500;
default:
return 0;
} }
} }
// 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() { 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");
$accessToken = $this->yellow->system->get("mastodonapiAccessToken"); $accessToken = $this->yellow->system->get("mastodonapiAccessToken");
$hashtag = ltrim($this->yellow->system->get("mastodonapiHashtag"), "#"); $hashtag = ltrim($this->yellow->system->get("mastodonapiHashtag"), '#');
$defaultAuthor = $this->yellow->system->get("mastodonapiAuthor"); $defaultAuthor = $this->yellow->system->get("mastodonapiAuthor");
$defaultTag = $this->yellow->system->get("mastodonapiTag"); $defaultTag = $this->yellow->system->get("mastodonapiTag");
$targetFolder = $this->yellow->system->get("mastodonapiFolder"); $targetFolder = $this->yellow->system->get("mastodonapiFolder");
$statusSetting = $this->yellow->system->get("mastodonapiStatus"); $limit = intval($this->yellow->system->get("mastodonapiLimit"));
$maxAgeHours = intval($this->yellow->system->get("mastodonapiMaxAge"));
$limit = intval($this->yellow->system->get("mastodonapiLimit"));
if ( empty($instanceUrl) || empty($accountId) ) return false; if (empty($instanceUrl) || empty($accountId)) {
return false;
}
$apiUrl = rtrim($instanceUrl, "/") . "/api/v1/accounts/" . $accountId . "/statuses?limit=" . $limit; $apiUrl = rtrim($instanceUrl, "/") . "/api/v1/accounts/" . $accountId . "/statuses?limit=" . $limit;
$headers = array("User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) YellowMastodonapi/" . self::VERSION); $headers = ["User-Agent: YellowMastodonAPI"];
if ( !empty($accessToken) ) $headers[] = "Authorization: Bearer " . $accessToken; if (!empty($accessToken)) {
$headers[] = "Authorization: Bearer " . $accessToken;
}
$ch = curl_init($apiUrl); $ch = curl_init($apiUrl);
curl_setopt_array($ch, array( curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true, CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 30, CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => $headers, CURLOPT_HTTPHEADER => $headers,
CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 0 CURLOPT_SSL_VERIFYHOST => 2
)); ]);
$json = curl_exec($ch); $json = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch); curl_close($ch);
if ( $json===false || $httpCode < 200 || $httpCode >= 300 ) return false; if ($json === false || $httpCode < 200 || $httpCode >= 300) {
return false;
}
$posts = json_decode($json, true); $posts = json_decode($json, true);
if ( !is_array($posts) ) return false; if (!is_array($posts)) {
return false;
echo "DEBUG: " . count($posts) . " Posts von API empfangen.\n"; }
$config = $this->getYellowConfig();
$postsCreated = 0; $postsCreated = 0;
foreach ( $posts as $index => $post ) {
// Snac-Verschachtelung auflösen: Wenn ein echtes 'reblog'-Objekt von jemand anderem drinsteckt
if ( !empty($post["reblog"]) && is_array($post["reblog"]) ) {
// Nur überspringen, wenn es wirklich der Post eines ANDEREN Nutzers ist, den du geteilt hast
if ( !empty($post["reblog"]["account"]["id"]) && $post["reblog"]["account"]["id"] != $accountId ) {
echo "DEBUG [$index]: Übersprungen (Echter Reblog eines fremden Beitrags)\n";
continue;
}
// Wenn es dein eigener ist, nutzen wir die inneren Daten
$post = $post["reblog"];
}
$contentHtml = $post["content"] ?? ""; foreach ($posts as $post) {
$createdAt = $post["created_at"] ?? ""; if (!empty($post['reblog']) || (!empty($post['reblogged']) && $post['reblogged'] === true)) continue;
$shortText = mb_substr(strip_tags($contentHtml), 0, 40) . "..."; if (!is_null($post['in_reply_to_id'])) continue;
// Reblogged-Flag nur werten, wenn es kein Eigen-Boost ist $contentHtml = $post['content'] ?? '';
if ( (isset($post["reblogged"]) && $post["reblogged"]===true) && empty($contentHtml) ) { $createdAt = $post['created_at'] ?? '';
echo "DEBUG [$index]: Übersprungen (Leerer Boost): $shortText\n"; $tags = array_column($post['tags'] ?? [], 'name');
continue;
}
// REPLY CHECK (Snac sendet leere Strings oder null) if (!empty($hashtag) && !in_array(strtolower($hashtag), array_map('strtolower', $tags))) {
if ( !empty($post["in_reply_to_id"]) ) {
echo "DEBUG [$index]: Übersprungen (Antwort - Reply-ID: " . $post["in_reply_to_id"] . "): $shortText\n";
continue;
}
// ALTER CHECK
if ( $maxAgeHours > 0 ) {
if ( strtotime($createdAt) < ( time() - ( $maxAgeHours * 3600 ) ) ) {
echo "DEBUG [$index]: Übersprungen (Zu alt - Datum: $createdAt): $shortText\n";
continue;
}
}
// HASHTAG CHECK
$tags = array_column($post["tags"] ?? array(), "name");
if ( !empty($hashtag) && !in_array(strtolower($hashtag), array_map("strtolower", $tags)) ) {
echo "DEBUG [$index]: Übersprungen (Hashtag #$hashtag fehlt): $shortText\n";
continue; continue;
} }
$title = $this->generateTitle($contentHtml); $title = $this->generateTitle($contentHtml);
$titleSlug = $this->generateTitleSlug($title); $titleSlug = $this->generateTitleSlug($title);
$fileName = date("Y-m-d", strtotime($createdAt)) . "-" . $titleSlug . ".md"; $datePrefix = date('Y-m-d', strtotime($createdAt));
$fullPath = $this->yellow->system->get("coreServerDocument") . $targetFolder . $fileName; $fileName = $datePrefix . '-' . $titleSlug . '.md';
$fullPath = $config->get("coreServerDocument") . $targetFolder . $fileName;
// EXISTENZ CHECK if (file_exists($fullPath)) continue;
if ( file_exists($fullPath) ) { if (!is_dir(dirname($fullPath))) mkdir(dirname($fullPath), 0755, true);
echo "DEBUG [$index]: Übersprungen (Datei existiert bereits: $fileName): $shortText\n";
continue;
}
if ( !is_dir(dirname($fullPath)) ) { $authorName = $post['account']['display_name'] ?: $defaultAuthor;
mkdir(dirname($fullPath), 0777, true); $authorUrl = $post['account']['url'] ?: "#";
chmod(dirname($fullPath), 0777);
}
$imageFilenames = array(); $imageFilenames = [];
$imageAltTexts = array(); $imageAltTexts = [];
foreach ( $post["media_attachments"] ?? array() as $media ) {
if ( ( $media["type"] ?? "" )==="image" ) { foreach ($post['media_attachments'] ?? [] as $media) {
$filename = $this->downloadAndSaveImage($media, $this->yellow->system->get("coreServerDocument") . "media/images/"); if (($media['type'] ?? '') === 'image') {
if ( $filename ) { $filename = $this->downloadAndSaveImage($media, $config->get("coreServerDocument") . 'media/images/');
if ($filename) {
$imageFilenames[] = $filename; $imageFilenames[] = $filename;
$imageAltTexts[] = $media["description"] ?? ""; $imageAltTexts[] = $media['description'] ?? '';
} }
} }
} }
$authorName = !empty($post["account"]["display_name"]) ? $post["account"]["display_name"] : $defaultAuthor;
$authorUrl = !empty($post["account"]["url"]) ? $post["account"]["url"] : "#";
$markdownContent = $this->generateMarkdown( $markdownContent = $this->generateMarkdown(
$title, $titleSlug, $createdAt, $contentHtml, $title,
$authorName, $authorUrl, $titleSlug,
$defaultAuthor, $defaultTag, $imageFilenames, $imageAltTexts, $statusSetting $createdAt,
$contentHtml,
$authorName,
$authorUrl,
$defaultAuthor,
$defaultTag,
$imageFilenames,
$imageAltTexts
); );
file_put_contents($fullPath, $markdownContent); file_put_contents($fullPath, $markdownContent);
chmod($fullPath, 0666);
echo "-> ERSTELLT [$index]: $fileName ($shortText)\n";
$postsCreated++; $postsCreated++;
} }
return $postsCreated; return $postsCreated;
} }
// Download and save image
private function downloadAndSaveImage($media, $dirPath) { private function downloadAndSaveImage($media, $dirPath) {
$imageUrl = $media['remote_url'] $imageUrl = $media['remote_url']
?? $media['url'] ?? $media['url']
@ -199,37 +156,30 @@ class YellowMastodonapi {
?? ($media['meta']['original']['url'] ?? null) ?? ($media['meta']['original']['url'] ?? null)
?? ($media['meta']['small']['url'] ?? null); ?? ($media['meta']['small']['url'] ?? null);
if (empty($imageUrl)) return false; if (empty($imageUrl)) {
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));
if (!strpos($fileName, '.')) {
$fileName .= ".jpg";
}
$fullPath = $dirPath . $fileName; $fullPath = $dirPath . $fileName;
if (file_exists($fullPath)) return $fileName;
if (file_exists($fullPath) && filesize($fullPath) > 0) return $fileName;
$ch = curl_init($imageUrl); $ch = curl_init($imageUrl);
$headers = ["User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) YellowMastodonAPI"]; $headers = ["User-Agent: YellowMastodonAPI"];
$accessToken = $this->yellow->system->get("mastodonapiAccessToken"); $accessToken = $this->yellow->system->get("mastodonapiAccessToken");
if (!empty($accessToken)) { if (!empty($accessToken)) {
$headers[] = "Authorization: Bearer " . $accessToken; $headers[] = "Authorization: Bearer " . $accessToken;
} }
curl_setopt_array($ch, [ curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true, CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 30, CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => $headers, CURLOPT_HTTPHEADER => $headers,
CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 0 CURLOPT_SSL_VERIFYHOST => 2,
]); ]);
$imgData = curl_exec($ch); $imgData = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch); curl_close($ch);
@ -242,55 +192,105 @@ class YellowMastodonapi {
return $fileName; return $fileName;
} }
// Generate title from content
private function generateTitle($content) { private function generateTitle($content) {
$text = trim(strip_tags($content)); $text = strip_tags($content);
$text = preg_replace("/https?:\/\/[^\s]+|#\w+/u", "", $text); $text = preg_replace('/https?:\/\/[^\s]+|#\w+/u', '', $text);
$words = explode(" ", $text); $text = preg_replace('/[^\p{L}\p{N}\s-]/u', '', $text);
return trim(mb_substr(implode(" ", array_slice($words, 0, 7)), 0, 100, "UTF-8")) ?: "Mastodon Post"; $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";
} }
// Generate slug from title
private function generateTitleSlug($title) { private function generateTitleSlug($title) {
$slug = mb_strtolower($title, "UTF-8"); $slug = mb_strtolower($title, 'UTF-8');
$slug = str_replace(array("ä","ö","ü","ß"," "), array("ae","oe","ue","ss","-"), $slug); $slug = str_replace(['ä','ö','ü','Ä','Ö','Ü','ß',' '], ['ae','oe','ue','ae','oe','ue','ss','-'], $slug);
return trim(preg_replace("/[^a-z0-9-]+/", "", $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 = []) {
private function generateMarkdown($title, $slug, $dateTime, $html, $origAuthor, $origUrl, $yellowAuthor, $yellowTag, $images, $alts, $status) {
$date = new DateTime($dateTime); $date = new DateTime($dateTime);
$pub = $date->format("Y-m-d H:i:s"); $formattedDate = $date->format('Y-m-d');
$content = trim(html_entity_decode(strip_tags(str_replace(array("<br>", "</p>"), array("\n", "</p>\n"), $html)), ENT_QUOTES | ENT_HTML5, "UTF-8")); $formattedTime = $date->format('H:i:s');
$content = str_replace(array("\n", "#"), array("\n> ", "&#35;"), $content);
$imageMarkdown = ""; $content = strip_tags($html);
$featuredImage = $featuredImageAlt = ""; $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5, 'UTF-8');
foreach ( $images as $i => $filename ) {
$alt = isset($alts[$i]) ? htmlspecialchars($alts[$i], ENT_QUOTES) : ""; $hashtagString = '';
$imageMarkdown .= "\n<img src=\"/media/images/$filename\" alt=\"$alt\"/>\n"; if (preg_match('/(\s*(#\w+))+$/u', $content, $matches)) {
if ( $i===0 ) { $content = substr($content, 0, -strlen($matches[0]));
$featuredImage = "Image: $filename\n"; if (preg_match_all('/#(\w+)/u', $matches[0], $tags)) {
$featuredImageAlt = "ImageAlt: $alt\n"; $safeHashtags = array_map(function($tag) {
return str_replace('#', '&num;', $tag);
}, $tags[0]);
$hashtagString = implode(' ', $safeHashtags);
} }
} }
$authorSafe = str_replace("@", "&#64;", $origAuthor);
return "---\n" . $content = trim(preg_replace('/\s+/', ' ', $content));
"Title: $title\n" .
"TitleSlug: $slug\n" . $imageMarkdown = '';
"Published: $pub\n" . foreach ($images as $i => $filename) {
"Author: $yellowAuthor\n" . $alt = isset($alts[$i]) ? htmlspecialchars($alts[$i], ENT_QUOTES) : '';
"Layout: blog\n" . $imageMarkdown .= "\n<img src=\"/media/images/" . $filename . "\" alt=\"" . $alt . "\"/>\n";
"Tag: $yellowTag\n" . }
"Status: $status\n" .
$featuredImage . $featuredImageAlt . $featuredImage = '';
"---\n\n" . $featuredImageAlt = '';
$imageMarkdown . "\n" . if (!empty($images)) {
"> " . $content . "\n>\n" . $featuredImage = "Image: " . $images[0] . "\n";
"> — <a target=\"_blank\" href=\"$origUrl\">$authorSafe</a>"; if (isset($alts[0]) && !empty($alts[0])) {
$featuredImageAlt = "ImageAlt: " . htmlspecialchars($alts[0], ENT_QUOTES) . "\n";
}
}
$htmlSafeAuthorName = str_replace('@', '&#64;', $origAuthor);
$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
$featuredImage$featuredImageAlt---
$imageMarkdown
$blockquote
MD;
} }
// Return status file path private function getYellowConfig() {
private function getStatusFile() { return $this->yellow ? $this->yellow->system : null;
return $this->yellow->system->get("coreWorkerDirectory") . "mastodonapi.status"; }
private function sendResponse($message, $statusCode = 200) {
if (!headers_sent()) {
header("HTTP/1.1 $statusCode");
header("Content-Type: text/html; charset=UTF-8");
}
// Basis-URL aus der aktuellen Anfrage bestimmen
$fullUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http")
. "://{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}";
// Trigger-Pfad aus der Yellow-Konfiguration holen
$triggerPath = $this->yellow->system->get("mastodonapiTriggerPath");
// Ziel-URL: Alles vor dem Trigger-Pfad abschneiden
$redirectUrl = str_replace($triggerPath, "/", $fullUrl);
echo "<!DOCTYPE html><html><head>";
echo "<meta http-equiv='refresh' content='5;url={$redirectUrl}'>";
echo "<title>Mastodon/GoToSocial API</title></head><body>";
echo "<p>" . htmlspecialchars($message) . "</p>";
echo "<p>You will be redirected in 5 seconds...</p>";
echo "</body></html>";
exit;
} }
} }

View file

@ -1 +0,0 @@
0000000000

View file

@ -1,52 +0,0 @@
<p align="right"><a href="https://git.thepipes.ch/simon/yellow-mastodonapi/src/branch/main/readme-de.md">Deutsch</a> &nbsp; <a href="https://git.thepipes.ch/simon/yellow-mastodonapi/src/branch/main/readme.md">English</a></p>
# Mastodonapi 0.9.9
Importiere deine Toots in deinen Blog. Getestet mit <a href="https://comam.es/what-is-snac">Snac</a>.
## Installation
<a href="https://git.thepipes.ch/simon/yellow-mastodonapi/archive/main.zip">Lade die ZIP-Datei herunter</a> und kopiere sie in deinen system/extensions Ordner. <a target="blank_" href="https://github.com/annaesvensson/yellow-update">Erfahre mehr über Erweiterungen</a>.
## Funktionsweise
Das Plugin arbeitet vollautomatisch. Es prüft im Hintergrund regelmäßig, ob neue Beiträge vorhanden sind.
Wenn du die Seite besuchst, fragt es deine Mastodon-API ab und erstellt automatisch Blog-Beiträge aus deinen Toots. Optional kannst du einen #Hashtag angeben, um nur bestimmte Beiträge zu importieren.
Es gibt drei Möglichkeiten, den Import auszulösen:
1. **Automatisch (Lazy Cron):** Sobald ein Besucher deine Website aufruft, prüft das Plugin, ob das Update-Intervall abgelaufen ist und lädt bei Bedarf neue Toots.
2. **Über das Terminal:** Du kannst den Import manuell auf dem Server starten mit: php yellow.php mastodonapi
3. **Über den System-Cronjob:** Falls dein Host Cronjobs unterstützt, kannst du den Befehl php yellow.php mastodonapi regelmäßig ausführen lassen (z. B. alle 30 Minuten).
## Einstellungen
Alle Einstellungen befinden sich in `system/extensions/yellow-system.ini`:
`MastodonapiInstanceUrl` = deine Mastodon Instanz-URL (z. B. https://mastodon.social)
`MastodonapiAccountId` = deine Mastodon User-ID (numerisch, z. B. 1234567890)
*Finde die ID eines Snac oder Mastodon Accounts: Gib "https://DEINE_INSTANZ_URL/api/v1/accounts/lookup?acct=PROFILNAME" in deinen Browser ein.*
*Finde die ID eines GoToSocial-Accounts <a href="https://git.thepipes.ch/simon/yellow-mastodonapi/src/branch/main/help-de.md">(siehe hier)</a>*
`MastodonapiAccessToken` = optional für einen Mastodon-Account, zwingend für einen GoToSocial-Account <a href="https://git.thepipes.ch/simon/yellow-mastodonapi/src/branch/main/help-de.md">(Wie komme ich zu einem Token?)</a>
`MastodonapiUpdateInterval` = Zeitintervall zwischen automatischen Updates in Minuten (Standard: 30)
`MastodonapiLimit` = Anzahl der Toots, die pro Durchgang abgerufen werden (Standard: 20)
`MastodonapiHashtag` = leer lassen für alle Beiträge; oder mit #hashtag filtern
`MastodonapiAuthor` = Name, unter dem die Blog-Beiträge erscheinen sollen
`MastodonapiTag` = Tag für die Blog-Beiträge (Standard: Fediverse)
`MastodonapiFolder` = Pfad zum Blog-Ordner (Standard: content/2-blog/)
`MastodonapiMaxAge` = Alter der Toots in Stunden, die noch importiert werden sollen. 0 = alle verfügbaren Toots
`MastodonapiStatus` = Seitenstatus, Standardwert ist public
Die folgenden Status-Werte werden unterstützt:
`public` = Seite ist eine normale Seite
`draft` = Seite ist nicht sichtbar, Benutzer muss eingeloggt sein, erfordert die <a target="blank_" href="https://github.com/annaesvensson/yellow-draft">Draft-Erweiterung</a>
`unlisted` = Seite ist nicht sichtbar, kann aber mit dem richtigen Link aufgerufen werden
## Entwickler
Simon Rohr
*Fediverse: <a href="https://snac.thepipes.ch/simon">@simon@snac.thepipes.ch</a>*

42
readme-en.md Normal file
View file

@ -0,0 +1,42 @@
<p align="right"><a href="https://codeberg.org/simonpipe/yellow-mastodonapi/src/branch/main/readme.md">Deutsch</a> &nbsp; <a href="https://codeberg.org/simonpipe/yellow-mastodonapi/src/branch/main/readme-en.md">English</a></p>
# MastodonAPI
A plugin to import your public Toots via the Mastodon API into your Yellow Blog. The plugin works with Mastodon and with GoToSocial.
## Installation
1. Download the ZIP file
2. Copy it into the `/system/extensions` directory
3. Reload your website the plugin is now installed
## Usage
The plugin is triggered via a custom **webhook**.
When the webhook is called, it fetches your Toots from the Mastodon API and creates blog posts from them.
Optionally, you can specify a **#hashtag** to import only matching posts.
## Settings
All settings are in `system/extensions/yellow-system.ini`:
- **MastodonapiInstanceUrl** your Mastodon instance URL (e.g. `https://mastodon.social`)
- **MastodonapiAccountId** - your Mastodon user ID (numeric, e.g. 1234567890)
- Find the ID of a Mastodon account: Enter "https://YOUR_INSTANCE_URL/api/v1/accounts/lookup?acct=PROFILNAME" in your browser.
- Find the ID of a GoToSocial account <a href="https://codeberg.org/simonpipe/yellow-mastodonapi/src/branch/main/help-gotoso-en.md">(see here)</a>
- **MastodonapiAccessToken**
- Optional for a Mastodon account
- Mandatory for a GoToSocial account <a href="https://codeberg.org/simonpipe/yellow-mastodonapi/src/branch/main/help-gotoso-en.md">(see here)</a>
- **MastodonapiLimit** - Default value is 20
- **MastodonapiHashtag** leave empty to import all public posts; set to `#hashtag` to import only matching posts
- **MastodonapiAuthor** author name to use for imported blog posts
- **MastodonapiTag** tag to assign to imported blog posts
- **MastodonapiFolder** path to your blog folder (e.g. `content/1-blog/`)
- **MastodonapiTriggerPath** webhook path, e.g. `/mastodonapi-webhook-CHANGE_THIS_SECRET_KEY`
*(Be sure to change it! Anyone with this link can trigger the plugin.)*
💡 You can also call the webhook via a cron job to run the plugin automatically (e.g., every hour).
## Developer
Simon Pipe

View file

@ -1,52 +1,42 @@
<p align="right"><a href="https://git.thepipes.ch/simon/yellow-mastodonapi/src/branch/main/readme-de.md">Deutsch</a> &nbsp; <a href="https://git.thepipes.ch/simon/yellow-mastodonapi/src/branch/main/readme.md">English</a></p> <p align="right"><a href="https://codeberg.org/simonpipe/yellow-mastodonapi/src/branch/main/readme.md">Deutsch</a> &nbsp; <a href="https://codeberg.org/simonpipe/yellow-mastodonapi/src/branch/main/readme-en.md">English</a></p>
# Mastodonapi 0.9.9 # MastodonAPI
Import your toots into your blog. Tested with <a href="https://comam.es/what-is-snac">Snac</a>. Ein Plug-in, um deine öffentlichen Toots via Mastodon-API in deinen Yellow Blog zu importieren. Das Plugin funktiert mit Mastodon und mit GoToSocial.
## Installation ## Installation
<a href="https://git.thepipes.ch/simon/yellow-mastodonapi/archive/main.zip">Download ZIP file</a> and copy it into your `system/extensions` folder. <a target="blank_" href="https://github.com/annaesvensson/yellow-update">Learn more about extensions</a>. 1. ZIP-Datei herunterladen
2. In das Verzeichnis `/system/extensions` kopieren
3. Deine Website neu laden das Plug-in ist nun installiert
## How to ## Benutzung
The plugin works fully automatically. It regularly checks in the background if there are new posts. Das Plug-in wird über einen individuellen **Webhook** aufgerufen.
Beim Aufruf fragt es deine Mastodon-API ab und erstellt aus deinen Toots automatisch Blogbeiträge.
Optional kannst du einen **#Hashtag** angeben, um nur bestimmte Beiträge zu importieren.
When you visit the site, it queries your Mastodon API and automatically creates blog posts from your Toots. Optionally, you can specify a #Hashtag to import only certain posts. ## Einstellungen
There are three ways to trigger the import: Alle Einstellungen findest du in `system/extensions/yellow-system.ini`:
1. **Automatic (Lazy Cron):** As soon as a visitor accesses your website, the plugin checks if the update interval has expired and loads new Toots if necessary. - **MastodonapiInstanceUrl** URL deiner Mastodon-Instanz (z. B. `https://mastodon.social`)
2. **From the terminal:** You can manually start the import on the server with: `php yellow.php mastodonapi` - **MastodonapiAccountId** deine Mastodon-User-ID (numerisch, z. B. `1234567890`)
3. **From the system cronjob:** If your host supports cronjobs, you can have the command `php yellow.php mastodonapi` run regularly (e.g. every 30 minutes). - ID bei einem Mastodon-Account finden: Gib im Browser "https://DEINE_INSTANZ_URL/api/v1/accounts/lookup?acct=PROFILNAME" ein.
- ID bei einem GoToSocial-Account finden <a href="https://codeberg.org/simonpipe/yellow-mastodonapi/src/branch/main/help-gotoso-de.md">(siehe hier)</a>
- **MastodonapiAccessToken**
- Bei einem Mastodon-Account optional
- Bei einem GoToSocial-Account pflicht <a href="https://codeberg.org/simonpipe/yellow-mastodonapi/src/branch/main/help-gotoso-de.md">(siehe hier)</a>
- **MastodonapiLimit** - Standartwert ist 20
- **MastodonapiHashtag** leer lassen = alle öffentlichen Beiträge importieren; mit `#hashtag` = nur Beiträge mit diesem Tag
- **MastodonapiAuthor** Name, unter dem die Blogposts erscheinen sollen
- **MastodonapiTag** Tag, mit dem die Blogposts im Blog versehen werden
- **MastodonapiFolder** Pfad zum Blog-Ordner (z. B. `content/1-blog/`)
- **MastodonapiTriggerPath** Webhook-Pfad, z. B. `/mastodonapi-webhook-CHANGE_THIS_SECRET_KEY`
- Unbedingt anpassen! Wer diesen Link kennt, kann das Plug-in starten.
## Settings 💡 Du kannst den Webhook auch per Cronjob regelmäßig aufrufen (z. B. stündlich).
All settings are in `system/extensions/yellow-system.ini`: ## Entwickler
`MastodonapiInstanceUrl` = your Mastodon instance URL (e.g. https://mastodon.social) Simon Pipe
`MastodonapiAccountId` = your Mastodon user ID (numeric, e.g. 1234567890)
*Find the ID of a Snac or Mastodon account: Enter "https://YOUR_INSTANCE_URL/api/v1/accounts/lookup?acct=PROFILNAME" in your browser.*
*Find the ID of a GoToSocial account <a href="https://git.thepipes.ch/simon/yellow-mastodonapi/src/branch/main/help-en.md">(see here)</a>*
`MastodonapiAccessToken` = optional for a Mastodon account, mandatory for a GoToSocial account <a href="https://git.thepipes.ch/simon/yellow-mastodonapi/src/branch/main/help-en.md">(How do I get a token?)</a>
`MastodonapiUpdateInterval` = time interval between automatic updates in minutes (default: 30)
`MastodonapiLimit` = number of Toots retrieved per run (default: 20)
`MastodonapiHashtag` = leave blank for all posts; or filter with #hashtag
`MastodonapiAuthor` = name under which the blog posts should appear
`MastodonapiTag` = tag for the blog posts (default: Fediverse)
`MastodonapiFolder` = path to the blog folder (default: content/2-blog/)
`MastodonapiMaxAge` = age of Toots in hours that should still be imported. 0 = all available Toots
`MastodonapiStatus` = page status, default value is public
The following status values are supported:
`public` = page is a normal page
`draft` = page is not visible, user needs to log in, requires <a target="blank_" href="https://github.com/annaesvensson/yellow-draft">draft extension</a>
`unlisted` = page is not visible, but can be accessed with the correct link
## Developer
Simon Rohr
*Fediverse: <a href="https://snac.thepipes.ch/simon">@simon@snac.thepipes.ch</a>*