|
|
|
@ -5,6 +5,8 @@ import logging
|
|
|
|
|
import requests
|
|
|
|
|
import threading
|
|
|
|
|
import smtplib
|
|
|
|
|
import re
|
|
|
|
|
import unicodedata
|
|
|
|
|
from bs4 import BeautifulSoup
|
|
|
|
|
from io import BytesIO
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
color = "#2e7d32" if status == "success" else "#d32f2f"
|
|
|
|
|
bg_color = "#f5f5f5" if status == "success" else "#fff3f3"
|
|
|
|
@ -137,8 +178,12 @@ def save_seen_id(post_id):
|
|
|
|
|
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)
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -157,15 +202,21 @@ def fetch_og_data(url):
|
|
|
|
|
return None, None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def post_to_bluesky(message, link):
|
|
|
|
|
def post_to_bluesky(title, link, tags):
|
|
|
|
|
client = Client()
|
|
|
|
|
client.login(BSKY_HANDLE, BSKY_PASSWORD)
|
|
|
|
|
|
|
|
|
|
title, image_url = fetch_og_data(link)
|
|
|
|
|
text = title or message
|
|
|
|
|
hashtags = " ".join(f"#{tag}" for tag in tags) if tags else ""
|
|
|
|
|
message = f"{title}\n\n{link}"
|
|
|
|
|
if hashtags:
|
|
|
|
|
message += f"\n\n{hashtags}"
|
|
|
|
|
|
|
|
|
|
if title and image_url:
|
|
|
|
|
try:
|
|
|
|
|
facets = extract_facets_utf8(message) # <-- NEU
|
|
|
|
|
|
|
|
|
|
# Versuche OG-Vorschau
|
|
|
|
|
try:
|
|
|
|
|
og_title, image_url = fetch_og_data(link)
|
|
|
|
|
if og_title and image_url:
|
|
|
|
|
embed = {
|
|
|
|
|
"$type": "app.bsky.embed.external",
|
|
|
|
|
"external": {
|
|
|
|
@ -186,16 +237,18 @@ def post_to_bluesky(message, link):
|
|
|
|
|
blob = client.upload_blob(BytesIO(img_resp.content))
|
|
|
|
|
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.")
|
|
|
|
|
return
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Error uploading preview to Bluesky: {e}")
|
|
|
|
|
except Exception as 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.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def extract_post_date(entry):
|
|
|
|
|
date_fields = [
|
|
|
|
|
entry.get("published"),
|
|
|
|
@ -243,9 +296,19 @@ def main():
|
|
|
|
|
title = entry.get("title", "").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 = []
|
|
|
|
|
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:
|
|
|
|
|
hashtags = " ".join(f"#{tag}" for tag in tags)
|
|
|
|
@ -257,11 +320,11 @@ def main():
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
if POST_TARGETS in ("mastodon", "both"):
|
|
|
|
|
post_to_mastodon(message)
|
|
|
|
|
post_to_mastodon(title, link, tags)
|
|
|
|
|
time.sleep(2)
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
logger.info(f"✅ Post successfully published.")
|
|
|
|
|