Added function to post mastodon, or bluesky or both
This commit is contained in:
15
README.md
15
README.md
@ -2,7 +2,19 @@
|
|||||||
|
|
||||||
**BlueMastoFeed** ist ein Docker-basiertes Tool, das regelmäßig einen RSS-Feed ausliest und neue Beiträge automatisch auf **Mastodon** und **Bluesky** veröffentlicht.
|
**BlueMastoFeed** ist ein Docker-basiertes Tool, das regelmäßig einen RSS-Feed ausliest und neue Beiträge automatisch auf **Mastodon** und **Bluesky** veröffentlicht.
|
||||||
|
|
||||||
Es prüft, ob ein Beitrag bereits gepostet wurde, und speichert diese Information lokal in einer Datei (`/data/seen_posts.txt`). Optional werden OpenGraph-Daten (Titel, Vorschau-Bild etc.) der verlinkten Seiten extrahiert, um die Posts ansprechender zu gestalten.
|
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- RSS-Feed regelmäßig auslesen
|
||||||
|
- Postfilterung nach Alter (`MAX_POST_AGE_DAYS`)
|
||||||
|
- Verhindert doppelte Posts mit Hilfe einer persistierten ID-Liste
|
||||||
|
- Posten auf:
|
||||||
|
- ✅ Mastodon
|
||||||
|
- ✅ Bluesky
|
||||||
|
- ✅ Beides (konfigurierbar über `.env`)
|
||||||
|
- Optionaler E-Mail-Versand bei Erfolg oder Fehler
|
||||||
|
- Healthcheck-Endpoint auf Port 8000
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -87,6 +99,7 @@ Die folgenden Umgebungsvariablen steuern das Verhalten des Containers. Sie könn
|
|||||||
| ----------------------- | ------------------------------------------------------------ | -------------------------- | -------------- |
|
| ----------------------- | ------------------------------------------------------------ | -------------------------- | -------------- |
|
||||||
| `FEED_URL` | URL zum RSS- oder Atom-Feed | `https://example.com/feed` | _erforderlich_ |
|
| `FEED_URL` | URL zum RSS- oder Atom-Feed | `https://example.com/feed` | _erforderlich_ |
|
||||||
| `MAX_POST_AGE_DAYS` | Maximales Alter eines Beitrags (in Tagen), der gepostet werden darf | `0` = nur heutige Beiträge | `0` |
|
| `MAX_POST_AGE_DAYS` | Maximales Alter eines Beitrags (in Tagen), der gepostet werden darf | `0` = nur heutige Beiträge | `0` |
|
||||||
|
| `POST_TARGETS` | Zielplattform(en): `mastodon`, `bluesky`, `both` | `mastodon` = nur Mastodon | `both` |
|
||||||
| `MASTODON_API_BASE_URL` | Basis-URL deiner Mastodon-Instanz | `https://mastodon.social` | _erforderlich_ |
|
| `MASTODON_API_BASE_URL` | Basis-URL deiner Mastodon-Instanz | `https://mastodon.social` | _erforderlich_ |
|
||||||
| `MASTODON_ACCESS_TOKEN` | Access Token für die Mastodon API | `abc123...` | _erforderlich_ |
|
| `MASTODON_ACCESS_TOKEN` | Access Token für die Mastodon API | `abc123...` | _erforderlich_ |
|
||||||
| `BSKY_IDENTIFIER` | Bluesky-Handle | `name.bsky.social` | _erforderlich_ |
|
| `BSKY_IDENTIFIER` | Bluesky-Handle | `name.bsky.social` | _erforderlich_ |
|
||||||
|
@ -25,6 +25,7 @@ MASTODON_TOKEN = os.getenv("MASTODON_ACCESS_TOKEN")
|
|||||||
BSKY_HANDLE = os.getenv("BSKY_IDENTIFIER")
|
BSKY_HANDLE = os.getenv("BSKY_IDENTIFIER")
|
||||||
BSKY_PASSWORD = os.getenv("BSKY_PASSWORD")
|
BSKY_PASSWORD = os.getenv("BSKY_PASSWORD")
|
||||||
MAX_POST_AGE_DAYS = int(os.getenv("MAX_POST_AGE_DAYS", 0))
|
MAX_POST_AGE_DAYS = int(os.getenv("MAX_POST_AGE_DAYS", 0))
|
||||||
|
POST_TARGETS = os.getenv("POST_TARGETS", "both").lower()
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(logging.INFO)
|
||||||
@ -33,7 +34,6 @@ formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
|||||||
handler.setFormatter(formatter)
|
handler.setFormatter(formatter)
|
||||||
logger.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
|
|
||||||
|
|
||||||
class HealthHandler(BaseHTTPRequestHandler):
|
class HealthHandler(BaseHTTPRequestHandler):
|
||||||
"""Handles HTTP GET requests for the health check endpoint."""
|
"""Handles HTTP GET requests for the health check endpoint."""
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
@ -49,7 +49,6 @@ class HealthHandler(BaseHTTPRequestHandler):
|
|||||||
"""Suppress default HTTP request logging."""
|
"""Suppress default HTTP request logging."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def start_health_server():
|
def start_health_server():
|
||||||
"""Starts the health check HTTP server in a background thread."""
|
"""Starts the health check HTTP server in a background thread."""
|
||||||
server = HTTPServer(("0.0.0.0", 8000), HealthHandler)
|
server = HTTPServer(("0.0.0.0", 8000), HealthHandler)
|
||||||
@ -57,13 +56,11 @@ def start_health_server():
|
|||||||
thread.start()
|
thread.start()
|
||||||
logger.info("Healthcheck server is running on port 8000.")
|
logger.info("Healthcheck server is running on port 8000.")
|
||||||
|
|
||||||
|
|
||||||
def should_send_email(on_success: bool):
|
def should_send_email(on_success: bool):
|
||||||
"""Determines whether to send a status email based on mode and success."""
|
"""Determines whether to send a status email based on mode and success."""
|
||||||
mode = os.getenv("EMAIL_MODE", "errors").lower()
|
mode = os.getenv("EMAIL_MODE", "errors").lower()
|
||||||
return (mode == "all") or (mode == "errors" and not on_success)
|
return (mode == "all") or (mode == "errors" and not on_success)
|
||||||
|
|
||||||
|
|
||||||
def send_status_email(subject, html_content):
|
def send_status_email(subject, html_content):
|
||||||
"""Sends a formatted HTML email with the given subject and content."""
|
"""Sends a formatted HTML email with the given subject and content."""
|
||||||
try:
|
try:
|
||||||
@ -88,7 +85,6 @@ def send_status_email(subject, html_content):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error sending status email: {e}")
|
logger.error(f"Error sending status email: {e}")
|
||||||
|
|
||||||
|
|
||||||
def load_seen_ids():
|
def load_seen_ids():
|
||||||
"""Loads the set of already seen post IDs from file."""
|
"""Loads the set of already seen post IDs from file."""
|
||||||
os.makedirs(os.path.dirname(SEEN_POSTS_FILE), exist_ok=True)
|
os.makedirs(os.path.dirname(SEEN_POSTS_FILE), exist_ok=True)
|
||||||
@ -97,19 +93,16 @@ def load_seen_ids():
|
|||||||
with open(SEEN_POSTS_FILE, "r") as f:
|
with open(SEEN_POSTS_FILE, "r") as f:
|
||||||
return set(line.strip() for line in f)
|
return set(line.strip() for line in f)
|
||||||
|
|
||||||
|
|
||||||
def save_seen_id(post_id):
|
def save_seen_id(post_id):
|
||||||
"""Appends a new post ID to the seen posts file."""
|
"""Appends a new post ID to the seen posts file."""
|
||||||
with open(SEEN_POSTS_FILE, "a") as f:
|
with open(SEEN_POSTS_FILE, "a") as f:
|
||||||
f.write(post_id + "\n")
|
f.write(post_id + "\n")
|
||||||
|
|
||||||
|
|
||||||
def post_to_mastodon(message):
|
def post_to_mastodon(message):
|
||||||
"""Posts a message to Mastodon."""
|
"""Posts a message to Mastodon."""
|
||||||
mastodon = Mastodon(access_token=MASTODON_TOKEN, api_base_url=MASTODON_BASE_URL)
|
mastodon = Mastodon(access_token=MASTODON_TOKEN, api_base_url=MASTODON_BASE_URL)
|
||||||
mastodon.toot(message)
|
mastodon.toot(message)
|
||||||
|
|
||||||
|
|
||||||
def fetch_og_data(url):
|
def fetch_og_data(url):
|
||||||
"""Fetches Open Graph title and image URL from a web page."""
|
"""Fetches Open Graph title and image URL from a web page."""
|
||||||
try:
|
try:
|
||||||
@ -125,7 +118,6 @@ def fetch_og_data(url):
|
|||||||
logger.error(f"Error loading OG data: {e}")
|
logger.error(f"Error loading OG data: {e}")
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
def post_to_bluesky(message, link):
|
def post_to_bluesky(message, link):
|
||||||
"""Posts a message and optional preview to Bluesky."""
|
"""Posts a message and optional preview to Bluesky."""
|
||||||
client = Client()
|
client = Client()
|
||||||
@ -157,14 +149,13 @@ def post_to_bluesky(message, link):
|
|||||||
embed["external"]["thumb"] = blob.blob
|
embed["external"]["thumb"] = blob.blob
|
||||||
|
|
||||||
client.send_post(text=text, embed=embed)
|
client.send_post(text=text, embed=embed)
|
||||||
logger.info("Posted with OG preview.")
|
logger.info("Posted to Bluesky with OG preview.")
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error uploading OG preview: {e}")
|
logger.error(f"Error uploading OG preview: {e}")
|
||||||
|
|
||||||
client.send_post(f"{text}\n{link}")
|
client.send_post(f"{text}\n{link}")
|
||||||
logger.info("Posted without preview.")
|
logger.info("Posted to Bluesky without preview.")
|
||||||
|
|
||||||
|
|
||||||
def extract_post_date(entry):
|
def extract_post_date(entry):
|
||||||
"""Extracts the oldest available date from various RSS date fields."""
|
"""Extracts the oldest available date from various RSS date fields."""
|
||||||
@ -189,7 +180,6 @@ def extract_post_date(entry):
|
|||||||
|
|
||||||
return min(dates) if dates else datetime.now(timezone.utc)
|
return min(dates) if dates else datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Main function to process feed entries and post new items."""
|
"""Main function to process feed entries and post new items."""
|
||||||
seen_ids = load_seen_ids()
|
seen_ids = load_seen_ids()
|
||||||
@ -219,9 +209,13 @@ def main():
|
|||||||
logger.info(f"New post: {title}")
|
logger.info(f"New post: {title}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
post_to_mastodon(message)
|
if POST_TARGETS in ("mastodon", "both"):
|
||||||
time.sleep(2)
|
post_to_mastodon(message)
|
||||||
post_to_bluesky(message, link)
|
time.sleep(2)
|
||||||
|
|
||||||
|
if POST_TARGETS in ("bluesky", "both"):
|
||||||
|
post_to_bluesky(message, link)
|
||||||
|
|
||||||
save_seen_id(post_id)
|
save_seen_id(post_id)
|
||||||
logger.info("✅ Successfully posted.")
|
logger.info("✅ Successfully posted.")
|
||||||
|
|
||||||
@ -241,7 +235,6 @@ def main():
|
|||||||
|
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
INTERVAL_MINUTES = int(os.getenv("INTERVAL_MINUTES", 30))
|
INTERVAL_MINUTES = int(os.getenv("INTERVAL_MINUTES", 30))
|
||||||
logger.info(f"Start feed check every {INTERVAL_MINUTES} minutes.")
|
logger.info(f"Start feed check every {INTERVAL_MINUTES} minutes.")
|
||||||
|
3
env
3
env
@ -9,6 +9,9 @@ MASTODON_ACCESS_TOKEN=your_mastodon_access_token
|
|||||||
BSKY_IDENTIFIER=your_handle.bsky.social
|
BSKY_IDENTIFIER=your_handle.bsky.social
|
||||||
BSKY_PASSWORD=your_bluesky_password
|
BSKY_PASSWORD=your_bluesky_password
|
||||||
|
|
||||||
|
# mögliche Werte: mastodon, bluesky, both
|
||||||
|
POST_TARGETS=both
|
||||||
|
|
||||||
# Intervall in Minuten für Feedprüfung
|
# Intervall in Minuten für Feedprüfung
|
||||||
INTERVAL_MINUTES=30
|
INTERVAL_MINUTES=30
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user