4 Commits

Author SHA1 Message Date
3e1255ccdc fix(main): Fixed error in hashtag display 2025-06-02 17:22:00 +02:00
3bb33ca379 fix(app): Add hashtags to bluesky post 2025-06-02 16:27:49 +02:00
7fe2a1de00 chore(app): Release v0.9.6
All checks were successful
Build and Push Docker Image on Tag / build_and_push (push) Successful in 30s
Create Release / release (push) Successful in 10s
2025-06-01 14:38:19 +02:00
8f5813b39c fix(app): Add title to mastodon post 2025-06-01 13:49:25 +02:00
2 changed files with 77 additions and 14 deletions

View File

@@ -1,5 +1,5 @@
FROM python:3.11-slim FROM python:3.11-slim
LABEL version="0.9.0" LABEL version="0.9.6"
RUN apt-get update && apt-get install -y curl && apt-get clean && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get install -y curl && apt-get clean && rm -rf /var/lib/apt/lists/*

View File

@@ -5,6 +5,8 @@ import logging
import requests import requests
import threading import threading
import smtplib import smtplib
import re
import unicodedata
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from io import BytesIO from io import BytesIO
from mastodon import Mastodon from mastodon import Mastodon
@@ -61,6 +63,45 @@ def should_send_email(on_success: bool):
return (mode == "all") or (mode == "errors" and not on_success) return (mode == "all") or (mode == "errors" and not on_success)
def extract_facets_utf8(text: str):
import re
facets = []
def get_byte_range(char_start, char_end):
byte_start = len(text[:char_start].encode("utf-8"))
byte_end = len(text[:char_end].encode("utf-8"))
return byte_start, byte_end
# Hashtags
for match in re.finditer(r"#(\w+)", text):
tag = match.group(1)
char_start, char_end = match.span()
byte_start, byte_end = get_byte_range(char_start, char_end)
facets.append({
"index": {"byteStart": byte_start, "byteEnd": byte_end},
"features": [{
"$type": "app.bsky.richtext.facet#tag",
"tag": tag
}]
})
# Links
for match in re.finditer(r"https?://[^\s]+", text):
url = match.group(0)
char_start, char_end = match.span()
byte_start, byte_end = get_byte_range(char_start, char_end)
facets.append({
"index": {"byteStart": byte_start, "byteEnd": byte_end},
"features": [{
"$type": "app.bsky.richtext.facet#link",
"uri": url
}]
})
return facets
def generate_email_html(status: str, title: str, link: str, error_message: str = None) -> str: def generate_email_html(status: str, title: str, link: str, error_message: str = None) -> str:
color = "#2e7d32" if status == "success" else "#d32f2f" color = "#2e7d32" if status == "success" else "#d32f2f"
bg_color = "#f5f5f5" if status == "success" else "#fff3f3" bg_color = "#f5f5f5" if status == "success" else "#fff3f3"
@@ -137,8 +178,12 @@ def save_seen_id(post_id):
f.write(post_id + "\n") f.write(post_id + "\n")
def post_to_mastodon(message): def post_to_mastodon(title, link, tags):
mastodon = Mastodon(access_token=MASTODON_TOKEN, api_base_url=MASTODON_BASE_URL) mastodon = Mastodon(access_token=MASTODON_TOKEN, api_base_url=MASTODON_BASE_URL)
hashtags = " ".join(f"#{tag}" for tag in tags) if tags else ""
message = f"{title}\n\n{link}"
if hashtags:
message += f"\n\n{hashtags}"
mastodon.toot(message) mastodon.toot(message)
@@ -157,15 +202,21 @@ def fetch_og_data(url):
return None, None return None, None
def post_to_bluesky(message, link): def post_to_bluesky(title, link, tags):
client = Client() client = Client()
client.login(BSKY_HANDLE, BSKY_PASSWORD) client.login(BSKY_HANDLE, BSKY_PASSWORD)
title, image_url = fetch_og_data(link) hashtags = " ".join(f"#{tag}" for tag in tags) if tags else ""
text = title or message message = f"{title}\n\n{link}"
if hashtags:
message += f"\n\n{hashtags}"
if title and image_url: facets = extract_facets_utf8(message) # <-- NEU
try:
# Versuche OG-Vorschau
try:
og_title, image_url = fetch_og_data(link)
if og_title and image_url:
embed = { embed = {
"$type": "app.bsky.embed.external", "$type": "app.bsky.embed.external",
"external": { "external": {
@@ -186,16 +237,18 @@ def post_to_bluesky(message, link):
blob = client.upload_blob(BytesIO(img_resp.content)) blob = client.upload_blob(BytesIO(img_resp.content))
embed["external"]["thumb"] = blob.blob embed["external"]["thumb"] = blob.blob
client.send_post(text=text, embed=embed) client.send_post(text=message, embed=embed, facets=facets) # <-- facets hier
logger.info(f"✅ Posted to Bluesky with preview.") logger.info(f"✅ Posted to Bluesky with preview.")
return return
except Exception as e: except Exception as e:
logger.error(f"❌ Error uploading preview to Bluesky: {e}") logger.error(f"❌ Error uploading preview to Bluesky: {e}")
client.send_post(f"{text}\n{link}") # Fallback: Nur Text, aber mit Facets
client.send_post(text=message, facets=facets) # <-- facets hier
logger.info(f"💡 Posted to Bluesky without preview.") logger.info(f"💡 Posted to Bluesky without preview.")
def extract_post_date(entry): def extract_post_date(entry):
date_fields = [ date_fields = [
entry.get("published"), entry.get("published"),
@@ -243,9 +296,19 @@ def main():
title = entry.get("title", "").strip() title = entry.get("title", "").strip()
link = entry.get("link", "").strip() link = entry.get("link", "").strip()
def sanitize_tag(tag):
tag = tag.lower()
tag = unicodedata.normalize("NFKD", tag).encode("ascii", "ignore").decode("ascii")
tag = re.sub(r"\W+", "", tag)
return tag
tags = [] tags = []
if "tags" in entry: if "tags" in entry:
tags = [tag["term"] for tag in entry.tags if "term" in tag] raw_tags = [
tag.get("term") if isinstance(tag, dict) else getattr(tag, "term", None)
for tag in entry.tags
]
tags = [sanitize_tag(t) for t in raw_tags if t]
if tags: if tags:
hashtags = " ".join(f"#{tag}" for tag in tags) hashtags = " ".join(f"#{tag}" for tag in tags)
@@ -257,11 +320,11 @@ def main():
try: try:
if POST_TARGETS in ("mastodon", "both"): if POST_TARGETS in ("mastodon", "both"):
post_to_mastodon(message) post_to_mastodon(title, link, tags)
time.sleep(2) time.sleep(2)
if POST_TARGETS in ("bluesky", "both"): if POST_TARGETS in ("bluesky", "both"):
post_to_bluesky(f"{title}\n{link}", link) post_to_bluesky(title, link, tags)
save_seen_id(post_id) save_seen_id(post_id)
logger.info(f"✅ Post successfully published.") logger.info(f"✅ Post successfully published.")