Chats, plantillas, movidas varias.

This commit is contained in:
Jesús Pavón Abián
2026-02-03 13:28:12 +01:00
parent 5f9cf2c25b
commit 7754cccc2e
10 changed files with 596 additions and 133 deletions

View File

@@ -222,42 +222,65 @@ def compose_notification(notification, db, settings, relative_times, show_screen
# Notification reason/type
reason = g(notification, "reason", "unknown")
# Get post text if available
# Get post text - try multiple locations depending on notification type
record = g(notification, "record", {})
post_text = ""
# For mentions, replies, quotes: text is in the record itself
post_text = g(record, "text", "")
# Format like Mastodon: "{username} has action: {status}"
# For likes and reposts: try to get the subject post text
if not post_text and reason in ("like", "repost"):
# First check for hydrated subject text (added by NotificationBuffer)
post_text = g(notification, "_subject_text", "")
# Check if there's a reasonSubject with embedded post data
if not post_text:
reason_subject = g(notification, "reasonSubject") or g(notification, "reason_subject")
if reason_subject:
# Sometimes the subject post is embedded
subject_record = g(reason_subject, "record", {})
post_text = g(subject_record, "text", "")
# Check if there's subject post data in other locations
if not post_text:
subject = g(record, "subject", {})
subject_text = g(subject, "text", "")
if subject_text:
post_text = subject_text
# Format: action text without username (username is already in column 0)
if reason == "like":
if post_text:
text = _("{username} has added to favorites: {status}").format(username=user_str, status=post_text)
text = _("has added to favorites: {status}").format(status=post_text)
else:
text = _("{username} has added to favorites").format(username=user_str)
text = _("has added to favorites")
elif reason == "repost":
if post_text:
text = _("{username} has reposted: {status}").format(username=user_str, status=post_text)
text = _("has reposted: {status}").format(status=post_text)
else:
text = _("{username} has reposted").format(username=user_str)
text = _("has reposted")
elif reason == "follow":
text = _("{username} has followed you.").format(username=user_str)
text = _("has followed you.")
elif reason == "mention":
if post_text:
text = _("{username} has mentioned you: {status}").format(username=user_str, status=post_text)
text = _("has mentioned you: {status}").format(status=post_text)
else:
text = _("{username} has mentioned you").format(username=user_str)
text = _("has mentioned you")
elif reason == "reply":
if post_text:
text = _("{username} has replied: {status}").format(username=user_str, status=post_text)
text = _("has replied: {status}").format(status=post_text)
else:
text = _("{username} has replied").format(username=user_str)
text = _("has replied")
elif reason == "quote":
if post_text:
text = _("{username} has quoted your post: {status}").format(username=user_str, status=post_text)
text = _("has quoted your post: {status}").format(status=post_text)
else:
text = _("{username} has quoted your post").format(username=user_str)
text = _("has quoted your post")
elif reason == "starterpack-joined":
text = _("{username} has joined your starter pack.").format(username=user_str)
text = _("has joined your starter pack.")
else:
text = f"{user_str}: {reason}"
text = reason
# Date
indexed_at = g(notification, "indexedAt", "") or g(notification, "indexed_at", "")
@@ -315,10 +338,10 @@ def compose_user(user, db, settings, relative_times, show_screen_names=False, sa
ts = ""
# Format like Mastodon: "Name (@handle). X followers, Y following, Z posts. Joined date"
if ts:
return [_("%s (@%s). %s followers, %s following, %s posts. Joined %s") % (display_name, handle, followers, following, posts, ts)]
else:
return [_("%s (@%s). %s followers, %s following, %s posts.") % (display_name, handle, followers, following, posts)]
# Use the exact same translatable string as Mastodon (sessions/mastodon/compose.py)
if not ts:
ts = _("unknown")
return [_("%s (@%s). %s followers, %s following, %s posts. Joined %s") % (display_name, handle, followers, following, posts, ts)]
def compose_convo(convo, db, settings, relative_times, show_screen_names=False, safe=True):

View File

@@ -528,12 +528,18 @@ class Session(base.baseSession):
api = self._ensure_client()
if not actors:
return {"items": []}
try:
res = api.app.bsky.actor.get_profiles({"actors": actors})
return {"items": getattr(res, "profiles", []) or []}
except Exception:
log.exception("Error fetching Bluesky profiles batch")
return {"items": []}
# API limit is 25 actors per request, batch if needed
all_profiles = []
batch_size = 25
for i in range(0, len(actors), batch_size):
batch = actors[i:i + batch_size]
try:
res = api.app.bsky.actor.get_profiles({"actors": batch})
profiles = getattr(res, "profiles", []) or []
all_profiles.extend(profiles)
except Exception:
log.exception("Error fetching Bluesky profiles batch")
return {"items": all_profiles}
def get_post_likes(self, uri: str, limit: int = 50, cursor: str | None = None) -> dict[str, Any]:
api = self._ensure_client()
@@ -734,24 +740,57 @@ class Session(base.baseSession):
api = self._ensure_client()
# Chat API requires using the chat proxy
dm_client = api.with_bsky_chat_proxy()
res = dm_client.chat.bsky.convo.list_convos({"limit": limit, "cursor": cursor})
return {"items": res.convos, "cursor": res.cursor}
dm = dm_client.chat.bsky.convo
params = {"limit": limit}
if cursor:
params["cursor"] = cursor
try:
res = dm.list_convos(params)
return {"items": res.convos, "cursor": getattr(res, "cursor", None)}
except Exception:
log.exception("Error listing conversations")
return {"items": [], "cursor": None}
def get_convo_messages(self, convo_id: str, limit: int = 50, cursor: str | None = None) -> dict[str, Any]:
api = self._ensure_client()
dm_client = api.with_bsky_chat_proxy()
res = dm_client.chat.bsky.convo.get_messages({"convoId": convo_id, "limit": limit, "cursor": cursor})
return {"items": res.messages, "cursor": res.cursor}
dm = dm_client.chat.bsky.convo
params = {"convoId": convo_id, "limit": limit}
if cursor:
params["cursor"] = cursor
try:
res = dm.get_messages(params)
return {"items": res.messages, "cursor": getattr(res, "cursor", None)}
except Exception:
log.exception("Error getting conversation messages")
return {"items": [], "cursor": None}
def send_chat_message(self, convo_id: str, text: str) -> Any:
api = self._ensure_client()
dm_client = api.with_bsky_chat_proxy()
return dm_client.chat.bsky.convo.send_message({
"convoId": convo_id,
"message": {
"text": text
}
})
dm = dm_client.chat.bsky.convo
try:
return dm.send_message({
"convoId": convo_id,
"message": {
"text": text
}
})
except Exception:
log.exception("Error sending chat message")
raise
def get_or_create_convo(self, members: list[str]) -> dict[str, Any] | None:
"""Get or create a conversation with the given members (DIDs)."""
api = self._ensure_client()
dm_client = api.with_bsky_chat_proxy()
dm = dm_client.chat.bsky.convo
try:
res = dm.get_convo_for_members({"members": members})
return res.convo
except Exception:
log.exception("Error getting/creating conversation")
return None
# Streaming/Polling methods

View File

@@ -227,32 +227,42 @@ def render_notification(notification, template, post_template, settings, relativ
screen_name = _g(author, "handle", "")
reason = _g(notification, "reason", "unknown")
record = _g(notification, "record") or {}
# Get post text - try multiple locations depending on notification type
post_text = _g(record, "text", "") or ""
# For likes and reposts: try to get the subject post text
if not post_text and reason in ("like", "repost"):
# First check for hydrated subject text (added by NotificationBuffer)
post_text = _g(notification, "_subject_text", "") or ""
# Check if there's a reasonSubject with embedded post data
if not post_text:
reason_subject = _g(notification, "reasonSubject") or _g(notification, "reason_subject")
if reason_subject:
subject_record = _g(reason_subject, "record", {})
post_text = _g(subject_record, "text", "") or ""
# Check subject in record
if not post_text:
subject = _g(record, "subject", {})
post_text = _g(subject, "text", "") or ""
# Format: action text without username (username is already in display_name for template)
if reason == "like":
text = _("{username} has added to favorites: {status}").format(
username=display_name, status=post_text
) if post_text else _("{username} has added to favorites").format(username=display_name)
text = _("has added to favorites: {status}").format(status=post_text) if post_text else _("has added to favorites")
elif reason == "repost":
text = _("{username} has reposted: {status}").format(
username=display_name, status=post_text
) if post_text else _("{username} has reposted").format(username=display_name)
text = _("has reposted: {status}").format(status=post_text) if post_text else _("has reposted")
elif reason == "follow":
text = _("{username} has followed you.").format(username=display_name)
text = _("has followed you.")
elif reason == "mention":
text = _("{username} has mentioned you: {status}").format(
username=display_name, status=post_text
) if post_text else _("{username} has mentioned you").format(username=display_name)
text = _("has mentioned you: {status}").format(status=post_text) if post_text else _("has mentioned you")
elif reason == "reply":
text = _("{username} has replied: {status}").format(
username=display_name, status=post_text
) if post_text else _("{username} has replied").format(username=display_name)
text = _("has replied: {status}").format(status=post_text) if post_text else _("has replied")
elif reason == "quote":
text = _("{username} has quoted your post: {status}").format(
username=display_name, status=post_text
) if post_text else _("{username} has quoted your post").format(username=display_name)
text = _("has quoted your post: {status}").format(status=post_text) if post_text else _("has quoted your post")
else:
text = "{user}: {reason}".format(user=display_name or screen_name, reason=reason)
text = reason
indexed_at = _g(notification, "indexedAt") or _g(notification, "indexed_at")
date = process_date(indexed_at, relative_times, offset_hours) if indexed_at else ""