This commit is contained in:
Jesús Pavón Abián
2026-02-01 19:15:31 +01:00
parent 13a9a6538d
commit c275ed9cf8
5 changed files with 243 additions and 446 deletions

View File

@@ -64,17 +64,7 @@ class Handler:
controller.accounts.append(name)
root_position = controller.view.search(name, name)
# Discover/home timeline
from pubsub import pub
pub.sendMessage(
"createBuffer",
buffer_type="home_timeline",
session_type="blueski",
buffer_title=_("Discover"),
parent_tab=root_position,
start=True,
kwargs=dict(parent=controller.view.nb, name="home_timeline", session=session)
)
# Home (Following-only timeline - reverse-chronological)
pub.sendMessage(
"createBuffer",
@@ -82,9 +72,19 @@ class Handler:
session_type="blueski",
buffer_title=_("Home"),
parent_tab=root_position,
start=False,
start=True,
kwargs=dict(parent=controller.view.nb, name="following_timeline", session=session)
)
# Discover timeline
pub.sendMessage(
"createBuffer",
buffer_type="home_timeline",
session_type="blueski",
buffer_title=_("Discover"),
parent_tab=root_position,
start=False,
kwargs=dict(parent=controller.view.nb, name="home_timeline", session=session)
)
# Mentions (replies, mentions, quotes)
pub.sendMessage(
"createBuffer",
@@ -140,7 +140,7 @@ class Handler:
"createBuffer",
buffer_type="FollowingBuffer",
session_type="blueski",
buffer_title=_("Followings"),
buffer_title=_("Following"),
parent_tab=root_position,
start=False,
kwargs=dict(parent=controller.view.nb, name="following", session=session)
@@ -209,8 +209,8 @@ class Handler:
start=False,
kwargs=dict(parent=controller.view.nb, name=buffer_name, session=session, query=query)
)
except Exception:
logger.exception("Failed to restore Bluesky search buffers")
except Exception as e:
logger.error("Failed to restore Bluesky search buffers: %s", e)
# Saved user timelines
try:
@@ -242,8 +242,8 @@ class Handler:
start=False,
kwargs=dict(parent=controller.view.nb, name=f"{handle}-timeline", session=session, actor=actor, handle=handle)
)
except Exception:
logger.exception("Failed to restore Bluesky timeline buffers")
except Exception as e:
logger.error("Failed to restore Bluesky timeline buffers: %s", e)
# Saved followers/following timelines
try:
@@ -279,8 +279,8 @@ class Handler:
start=False,
kwargs=dict(parent=controller.view.nb, name=f"{handle}-followers", session=session, actor=actor, handle=handle)
)
except Exception:
logger.exception("Failed to restore Bluesky followers buffers")
except Exception as e:
logger.error("Failed to restore Bluesky followers buffers: %s", e)
try:
following = session.settings["other_buffers"].get("following_timelines")
@@ -315,14 +315,14 @@ class Handler:
start=False,
kwargs=dict(parent=controller.view.nb, name=f"{handle}-following", session=session, actor=actor, handle=handle)
)
except Exception:
logger.exception("Failed to restore Bluesky following buffers")
except Exception as e:
logger.error("Failed to restore Bluesky following buffers: %s", e)
# Start the background poller for real-time-like updates
try:
session.start_streaming()
except Exception:
logger.exception("Failed to start Bluesky streaming for session %s", name)
except Exception as e:
logger.error("Failed to start Bluesky streaming for session %s: %s", name, e)
def start_buffer(self, controller, buffer):
"""Start a newly created Bluesky buffer."""
@@ -358,11 +358,11 @@ class Handler:
try:
buffer.session.settings["general"]["boost_mode"] = boost_mode
buffer.session.settings.write()
except Exception:
logger.exception("Failed to persist Bluesky boost_mode setting")
except Exception as e:
logger.error("Failed to persist Bluesky boost_mode setting: %s", e)
dlg.Destroy()
except Exception:
logger.exception("Error opening Bluesky account settings dialog")
except Exception as e:
logger.error("Error opening Bluesky account settings dialog: %s", e)
def user_details(self, buffer):
"""Show user profile dialog for the selected user/post."""
@@ -670,8 +670,8 @@ class Handler:
timelines.append(key)
session.settings["other_buffers"]["timelines"] = timelines
session.settings.write()
except Exception:
logger.exception("Failed to persist Bluesky timeline buffer")
except Exception as e:
logger.error("Failed to persist Bluesky timeline buffer: %s", e)
def _resolve_actor(self, session, user_payload):
def g(obj, key, default=None):
@@ -785,8 +785,8 @@ class Handler:
stored.append(key)
session.settings["other_buffers"][settings_key] = stored
session.settings.write()
except Exception:
logger.exception("Failed to persist Bluesky %s buffer", list_type)
except Exception as e:
logger.error("Failed to persist Bluesky %s buffer: %s", list_type, e)
def delete(self, buffer, controller):
"""Standard action for delete key / menu item"""
@@ -870,5 +870,5 @@ class Handler:
searches.append(query)
session.settings["other_buffers"]["searches"] = searches
session.settings.write()
except Exception:
logger.exception("Failed to save search to settings")
except Exception as e:
logger.error("Failed to save search to settings: %s", e)

View File

@@ -43,6 +43,10 @@ class BaseBuffer(base.Buffer):
self.bind_events()
def get_max_items(self):
"""Get max items per call from settings."""
return self.session.settings["general"]["max_posts_per_call"]
def create_buffer(self, parent, name):
# Default to HomePanel, can be overridden
self.buffer = BlueskiPanels.HomePanel(parent, name, account=self.account)
@@ -115,8 +119,8 @@ class BaseBuffer(base.Buffer):
original_date = arrow.get(indexed_at)
ts = original_date.humanize(locale=languageHandler.curLang[:2])
self.buffer.list.list.SetItem(index, 2, ts)
except Exception:
log.exception("Error updating relative time on focus")
except Exception as e:
log.error("Error updating relative time on focus: %s", e)
# Read long posts in GUI
if config.app["app-settings"].get("read_long_posts_in_gui", False) and self.buffer.list.list.HasFocus():
@@ -325,8 +329,8 @@ class BaseBuffer(base.Buffer):
self.buffer.list.list.SetItem(index, 1, post_data[1]) # Text
self.buffer.list.list.SetItem(index, 2, post_data[2]) # Date
# Note: compose_post returns 4 items but list only has 3 columns
except Exception:
log.exception("Error refreshing list item after like")
except Exception as e:
log.error("Error refreshing list item after like: %s", e)
def add_to_favorites(self, *args, **kwargs):
self.toggle_favorite(confirm=False)
@@ -370,8 +374,8 @@ class BaseBuffer(base.Buffer):
self.session.send_chat_message(convo_id, text)
self.session.sound.play("dm_sent.ogg")
output.speak(_("Message sent."), True)
except:
log.exception("Error sending Bluesky DM (invisible)")
except Exception as e:
log.error("Error sending Bluesky DM: %s", e)
output.speak(_("Failed to send message."), True)
dlg.Destroy()
return
@@ -392,8 +396,8 @@ class BaseBuffer(base.Buffer):
return
try:
blueski_messages.viewPost(self.session, item)
except Exception:
log.exception("Error opening Bluesky post viewer")
except Exception as e:
log.error("Error opening Bluesky post viewer: %s", e)
def url_(self, *args, **kwargs):
self.url()
@@ -596,8 +600,8 @@ class BaseBuffer(base.Buffer):
except Exception:
pass
output.speak(_("Deleted."))
except Exception:
log.exception("Error deleting Bluesky post")
except Exception as e:
log.error("Error deleting Bluesky post: %s", e)
output.speak(_("Could not delete."), True)

View File

@@ -20,19 +20,15 @@ class ConversationListBuffer(BaseBuffer):
self.buffer.session = self.session
def start_stream(self, mandatory=False, play_sound=True):
count = self.session.settings["general"].get("max_posts_per_call", 50)
count = self.get_max_items()
try:
res = self.session.list_convos(limit=count)
items = res.get("items", [])
# Clear to avoid list weirdness on refreshes?
# Chat list usually replaces content on fetch
self.session.db[self.name] = []
self.buffer.list.clear()
return self.process_items(items, play_sound)
except Exception:
log.exception("Error fetching conversations")
except Exception as e:
log.error("Error fetching conversations: %s", e)
return 0
def url(self, *args, **kwargs):
@@ -80,23 +76,18 @@ class ChatBuffer(BaseBuffer):
self.buffer.session = self.session
def start_stream(self, mandatory=False, play_sound=True):
if not self.convo_id: return 0
count = self.session.settings["general"].get("max_posts_per_call", 50)
if not self.convo_id:
return 0
count = self.get_max_items()
try:
res = self.session.get_convo_messages(self.convo_id, limit=count)
items = res.get("items", [])
# Message order in API is often Oldest...Newest or vice versa.
# We want them in order and only new ones.
# For chat, let's just clear and show last N messages for simplicity now.
self.session.db[self.name] = []
self.buffer.list.clear()
# API usually returns newest first. We want newest at bottom.
items = list(reversed(items))
return self.process_items(items, play_sound)
except Exception:
log.exception("Error fetching chat messages")
except Exception as e:
log.error("Error fetching chat messages: %s", e)
return 0
def on_reply(self, evt):

View File

@@ -3,11 +3,13 @@ import logging
import output
from .base import BaseBuffer
from wxUI.buffers.blueski import panels as BlueskiPanels
from pubsub import pub
log = logging.getLogger("controller.buffers.blueski.timeline")
class HomeTimeline(BaseBuffer):
"""Discover feed buffer."""
def __init__(self, *args, **kwargs):
super(HomeTimeline, self).__init__(*args, **kwargs)
self.type = "home_timeline"
@@ -16,90 +18,64 @@ class HomeTimeline(BaseBuffer):
self.sound = "tweet_received.ogg"
def create_buffer(self, parent, name):
# Override to use HomePanel
self.buffer = BlueskiPanels.HomePanel(parent, name)
self.buffer.session = self.session
def start_stream(self, mandatory=False, play_sound=True):
count = 50
try:
count = self.session.settings["general"].get("max_posts_per_call", 50)
except: pass
count = self.get_max_items()
api = self.session._ensure_client()
# Discover Logic
if not self.feed_uri:
self.feed_uri = self._resolve_discover_feed(api)
items = []
try:
res = None
if self.feed_uri:
# Fetch feed
res = api.app.bsky.feed.get_feed({"feed": self.feed_uri, "limit": count})
res = api.app.bsky.feed.get_feed({"feed": self.feed_uri, "limit": count})
else:
# Fallback to standard timeline
res = api.app.bsky.feed.get_timeline({"limit": count})
feed = getattr(res, "feed", [])
items = list(feed)
res = api.app.bsky.feed.get_timeline({"limit": count})
items = list(getattr(res, "feed", []))
self.next_cursor = getattr(res, "cursor", None)
except Exception:
log.exception("Failed to fetch home timeline")
except Exception as e:
log.error("Error fetching home timeline: %s", e)
return 0
return self.process_items(items, play_sound)
def get_more_items(self):
if not self.next_cursor:
return
count = 50
try:
count = self.session.settings["general"].get("max_posts_per_call", 50)
except:
pass
count = self.get_max_items()
api = self.session._ensure_client()
try:
if self.feed_uri:
res = api.app.bsky.feed.get_feed({"feed": self.feed_uri, "limit": count, "cursor": self.next_cursor})
else:
res = api.app.bsky.feed.get_timeline({"limit": count, "cursor": self.next_cursor})
feed = getattr(res, "feed", [])
items = list(feed)
items = list(getattr(res, "feed", []))
self.next_cursor = getattr(res, "cursor", None)
added = self.process_items(items, play_sound=False)
if added:
output.speak(_(u"%s items retrieved") % (str(added)), True)
except Exception:
log.exception("Error fetching more home timeline items")
output.speak(_(u"%s items retrieved") % added, True)
except Exception as e:
log.error("Error fetching more home timeline: %s", e)
def _resolve_discover_feed(self, api):
# Reuse logic from panels.py
cached = self.session.db.get("discover_feed_uri")
if cached:
return cached
try:
cached = self.session.db.get("discover_feed_uri")
if cached: return cached
res = api.app.bsky.feed.get_suggested_feeds({"limit": 50})
for feed in getattr(res, "feeds", []):
dn = getattr(feed, "displayName", "") or getattr(feed, "display_name", "")
if "discover" in dn.lower():
uri = getattr(feed, "uri", "")
self.session.db["discover_feed_uri"] = uri
return uri
except Exception:
pass
return None
# Simple fallback: Suggested feeds
try:
res = api.app.bsky.feed.get_suggested_feeds({"limit": 50})
feeds = getattr(res, "feeds", [])
for feed in feeds:
dn = getattr(feed, "displayName", "") or getattr(feed, "display_name", "")
if "discover" in dn.lower():
uri = getattr(feed, "uri", "")
self.session.db["discover_feed_uri"] = uri
try: self.session.save_persistent_data()
except: pass
return uri
except: pass
return None
except:
return None
class FollowingTimeline(BaseBuffer):
"""Following-only timeline (reverse-chronological)."""
def __init__(self, *args, **kwargs):
super(FollowingTimeline, self).__init__(*args, **kwargs)
self.type = "following_timeline"
@@ -107,50 +83,41 @@ class FollowingTimeline(BaseBuffer):
self.sound = "tweet_received.ogg"
def create_buffer(self, parent, name):
self.buffer = BlueskiPanels.HomePanel(parent, name) # Reuse HomePanel layout
self.buffer = BlueskiPanels.HomePanel(parent, name)
self.buffer.session = self.session
def start_stream(self, mandatory=False, play_sound=True):
count = 50
try: count = self.session.settings["general"].get("max_posts_per_call", 50)
except: pass
api = self.session._ensure_client()
try:
# Force reverse-chronological
res = api.app.bsky.feed.get_timeline({"limit": count, "algorithm": "reverse-chronological"})
feed = getattr(res, "feed", [])
items = list(feed)
self.next_cursor = getattr(res, "cursor", None)
except Exception:
log.exception("Error fetching following timeline")
return 0
return self.process_items(items, play_sound)
count = self.get_max_items()
api = self.session._ensure_client()
try:
res = api.app.bsky.feed.get_timeline({"limit": count, "algorithm": "reverse-chronological"})
items = list(getattr(res, "feed", []))
self.next_cursor = getattr(res, "cursor", None)
except Exception as e:
log.error("Error fetching following timeline: %s", e)
return 0
return self.process_items(items, play_sound)
def get_more_items(self):
if not self.next_cursor:
return
count = 50
try:
count = self.session.settings["general"].get("max_posts_per_call", 50)
except:
pass
count = self.get_max_items()
api = self.session._ensure_client()
try:
res = api.app.bsky.feed.get_timeline({"limit": count, "algorithm": "reverse-chronological", "cursor": self.next_cursor})
feed = getattr(res, "feed", [])
items = list(feed)
items = list(getattr(res, "feed", []))
self.next_cursor = getattr(res, "cursor", None)
added = self.process_items(items, play_sound=False)
if added:
output.speak(_(u"%s items retrieved") % (str(added)), True)
except Exception:
log.exception("Error fetching more following timeline items")
output.speak(_(u"%s items retrieved") % added, True)
except Exception as e:
log.error("Error fetching more following timeline: %s", e)
class NotificationBuffer(BaseBuffer):
"""Notifications buffer."""
def __init__(self, *args, **kwargs):
# Override compose_func before calling super().__init__
kwargs["compose_func"] = "compose_notification"
super(NotificationBuffer, self).__init__(*args, **kwargs)
self.type = "notifications"
@@ -162,60 +129,48 @@ class NotificationBuffer(BaseBuffer):
self.buffer.session = self.session
def start_stream(self, mandatory=False, play_sound=True):
count = 50
try:
count = self.session.settings["general"].get("max_posts_per_call", 50)
except Exception:
pass
count = self.get_max_items()
api = self.session._ensure_client()
if not api:
return 0
try:
res = api.app.bsky.notification.list_notifications({"limit": count})
notifications = getattr(res, "notifications", [])
notifications = list(getattr(res, "notifications", []))
self.next_cursor = getattr(res, "cursor", None)
if not notifications:
return 0
# Process notifications using the notification compose function
return self.process_items(list(notifications), play_sound)
except Exception:
log.exception("Error fetching Bluesky notifications")
return self.process_items(notifications, play_sound)
except Exception as e:
log.error("Error fetching notifications: %s", e)
return 0
def get_more_items(self):
if not self.next_cursor:
return
count = 50
try:
count = self.session.settings["general"].get("max_posts_per_call", 50)
except:
pass
count = self.get_max_items()
api = self.session._ensure_client()
if not api:
return
try:
res = api.app.bsky.notification.list_notifications({"limit": count, "cursor": self.next_cursor})
notifications = getattr(res, "notifications", [])
notifications = list(getattr(res, "notifications", []))
self.next_cursor = getattr(res, "cursor", None)
added = self.process_items(list(notifications), play_sound=False)
added = self.process_items(notifications, play_sound=False)
if added:
output.speak(_(u"%s items retrieved") % (str(added)), True)
except Exception:
log.exception("Error fetching more notifications")
output.speak(_(u"%s items retrieved") % added, True)
except Exception as e:
log.error("Error fetching more notifications: %s", e)
def add_new_item(self, notification):
"""Add a single new notification from streaming/polling."""
return self.process_items([notification], play_sound=True)
class Conversation(BaseBuffer):
"""Thread/conversation view."""
def __init__(self, *args, **kwargs):
super(Conversation, self).__init__(*args, **kwargs)
self.type = "conversation"
# We need the root URI or the URI of the post to show thread for
self.root_uri = kwargs.get("uri")
self.sound = "search_updated.ogg"
@@ -224,28 +179,20 @@ class Conversation(BaseBuffer):
self.buffer.session = self.session
def start_stream(self, mandatory=False, play_sound=True):
if not self.root_uri: return 0
if not self.root_uri:
return 0
api = self.session._ensure_client()
try:
params = {"uri": self.root_uri, "depth": 100, "parentHeight": 100}
try:
res = api.app.bsky.feed.get_post_thread(params)
except Exception:
res = api.app.bsky.feed.get_post_thread({"uri": self.root_uri})
def g(obj, key, default=None):
if isinstance(obj, dict):
return obj.get(key, default)
return getattr(obj, key, default)
thread = getattr(res, "thread", None) or (res.get("thread") if isinstance(res, dict) else None)
res = api.app.bsky.feed.get_post_thread({"uri": self.root_uri, "depth": 100, "parentHeight": 100})
thread = getattr(res, "thread", None)
if not thread:
return 0
final_items = []
def g(obj, key, default=None):
return obj.get(key, default) if isinstance(obj, dict) else getattr(obj, key, default)
# Add parent chain (oldest to newest) if available
final_items = []
# Add ancestors
ancestors = []
parent = g(thread, "parent")
while parent:
@@ -255,29 +202,28 @@ class Conversation(BaseBuffer):
parent = g(parent, "parent")
final_items.extend(ancestors)
# Traverse thread
def traverse(node):
if not node:
return
post = g(node, "post")
if post:
final_items.append(post)
replies = g(node, "replies") or []
for r in replies:
for r in (g(node, "replies") or []):
traverse(r)
traverse(thread)
# Clear existing items to avoid duplication when refreshing a thread view (which changes structure little)
self.session.db[self.name] = []
self.buffer.list.clear() # Clear UI too
self.buffer.list.clear()
return self.process_items(final_items, play_sound)
except Exception:
log.exception("Error fetching thread")
except Exception as e:
log.error("Error fetching thread: %s", e)
return 0
class LikesBuffer(BaseBuffer):
"""User's liked posts."""
def __init__(self, *args, **kwargs):
super(LikesBuffer, self).__init__(*args, **kwargs)
self.type = "likes"
@@ -289,50 +235,39 @@ class LikesBuffer(BaseBuffer):
self.buffer.session = self.session
def start_stream(self, mandatory=False, play_sound=True):
count = 50
try:
count = self.session.settings["general"].get("max_posts_per_call", 50)
except Exception:
pass
count = self.get_max_items()
api = self.session._ensure_client()
try:
res = api.app.bsky.feed.get_actor_likes({"actor": api.me.did, "limit": count})
items = getattr(res, "feed", None) or getattr(res, "items", None) or []
items = list(getattr(res, "feed", None) or getattr(res, "items", None) or [])
self.next_cursor = getattr(res, "cursor", None)
except Exception:
log.exception("Error fetching likes")
except Exception as e:
log.error("Error fetching likes: %s", e)
return 0
return self.process_items(list(items), play_sound)
return self.process_items(items, play_sound)
def get_more_items(self):
if not self.next_cursor:
return
count = 50
try:
count = self.session.settings["general"].get("max_posts_per_call", 50)
except:
pass
count = self.get_max_items()
api = self.session._ensure_client()
if not api:
return
try:
res = api.app.bsky.feed.get_actor_likes({"actor": api.me.did, "limit": count, "cursor": self.next_cursor})
items = getattr(res, "feed", None) or getattr(res, "items", None) or []
items = list(getattr(res, "feed", None) or getattr(res, "items", None) or [])
self.next_cursor = getattr(res, "cursor", None)
added = self.process_items(list(items), play_sound=False)
added = self.process_items(items, play_sound=False)
if added:
output.speak(_(u"%s items retrieved") % (str(added)), True)
except Exception:
log.exception("Error fetching more likes")
output.speak(_(u"%s items retrieved") % added, True)
except Exception as e:
log.error("Error fetching more likes: %s", e)
class MentionsBuffer(BaseBuffer):
"""Buffer for mentions and replies to the current user."""
"""Mentions, replies and quotes."""
def __init__(self, *args, **kwargs):
# Use notification compose function since mentions come from notifications
kwargs["compose_func"] = "compose_notification"
super(MentionsBuffer, self).__init__(*args, **kwargs)
self.type = "mentions"
@@ -344,46 +279,26 @@ class MentionsBuffer(BaseBuffer):
self.buffer.session = self.session
def start_stream(self, mandatory=False, play_sound=True):
count = 50
try:
count = self.session.settings["general"].get("max_posts_per_call", 50)
except Exception:
pass
count = self.get_max_items()
api = self.session._ensure_client()
if not api:
return 0
try:
res = api.app.bsky.notification.list_notifications({"limit": count})
notifications = getattr(res, "notifications", [])
self.next_cursor = getattr(res, "cursor", None)
if not notifications:
return 0
# Filter only mentions and replies
mentions = [
n for n in notifications
if getattr(n, "reason", "") in ("mention", "reply", "quote")
]
mentions = [n for n in notifications if getattr(n, "reason", "") in ("mention", "reply", "quote")]
if not mentions:
return 0
return self.process_items(mentions, play_sound)
except Exception:
log.exception("Error fetching Bluesky mentions")
except Exception as e:
log.error("Error fetching mentions: %s", e)
return 0
def get_more_items(self):
if not self.next_cursor:
return
count = 50
try:
count = self.session.settings["general"].get("max_posts_per_call", 50)
except:
pass
count = self.get_max_items()
api = self.session._ensure_client()
if not api:
return
@@ -391,98 +306,68 @@ class MentionsBuffer(BaseBuffer):
res = api.app.bsky.notification.list_notifications({"limit": count, "cursor": self.next_cursor})
notifications = getattr(res, "notifications", [])
self.next_cursor = getattr(res, "cursor", None)
# Filter only mentions and replies
mentions = [
n for n in notifications
if getattr(n, "reason", "") in ("mention", "reply", "quote")
]
mentions = [n for n in notifications if getattr(n, "reason", "") in ("mention", "reply", "quote")]
if mentions:
added = self.process_items(mentions, play_sound=False)
if added:
output.speak(_(u"%s items retrieved") % (str(added)), True)
except Exception:
log.exception("Error fetching more mentions")
output.speak(_(u"%s items retrieved") % added, True)
except Exception as e:
log.error("Error fetching more mentions: %s", e)
def add_new_item(self, notification):
"""Add a single new mention from streaming/polling."""
reason = getattr(notification, "reason", "")
if reason in ("mention", "reply", "quote"):
if getattr(notification, "reason", "") in ("mention", "reply", "quote"):
return self.process_items([notification], play_sound=True)
return 0
class SentBuffer(BaseBuffer):
"""Buffer for posts sent by the current user."""
"""User's sent posts."""
def __init__(self, *args, **kwargs):
super(SentBuffer, self).__init__(*args, **kwargs)
self.type = "sent"
self.next_cursor = None
# No sound for sent posts (user's own posts)
def create_buffer(self, parent, name):
self.buffer = BlueskiPanels.HomePanel(parent, name)
self.buffer.session = self.session
def start_stream(self, mandatory=False, play_sound=True):
count = 50
try:
count = self.session.settings["general"].get("max_posts_per_call", 50)
except Exception:
pass
count = self.get_max_items()
api = self.session._ensure_client()
if not api or not api.me:
return 0
try:
# Get author's own posts (excluding replies)
res = api.app.bsky.feed.get_author_feed({
"actor": api.me.did,
"limit": count,
"filter": "posts_no_replies"
})
items = getattr(res, "feed", [])
res = api.app.bsky.feed.get_author_feed({"actor": api.me.did, "limit": count, "filter": "posts_no_replies"})
items = list(getattr(res, "feed", []))
self.next_cursor = getattr(res, "cursor", None)
if not items:
return 0
return self.process_items(list(items), play_sound)
except Exception:
log.exception("Error fetching sent posts")
return self.process_items(items, play_sound)
except Exception as e:
log.error("Error fetching sent posts: %s", e)
return 0
def get_more_items(self):
if not self.next_cursor:
return
count = 50
try:
count = self.session.settings["general"].get("max_posts_per_call", 50)
except:
pass
count = self.get_max_items()
api = self.session._ensure_client()
if not api or not api.me:
return
try:
res = api.app.bsky.feed.get_author_feed({
"actor": api.me.did,
"limit": count,
"filter": "posts_no_replies",
"cursor": self.next_cursor
})
items = getattr(res, "feed", [])
res = api.app.bsky.feed.get_author_feed({"actor": api.me.did, "limit": count, "filter": "posts_no_replies", "cursor": self.next_cursor})
items = list(getattr(res, "feed", []))
self.next_cursor = getattr(res, "cursor", None)
added = self.process_items(list(items), play_sound=False)
added = self.process_items(items, play_sound=False)
if added:
output.speak(_(u"%s items retrieved") % (str(added)), True)
except Exception:
log.exception("Error fetching more sent posts")
output.speak(_(u"%s items retrieved") % added, True)
except Exception as e:
log.error("Error fetching more sent posts: %s", e)
class UserTimeline(BaseBuffer):
"""Buffer for posts by a specific user."""
"""Timeline for a specific user."""
def __init__(self, *args, **kwargs):
self.actor = kwargs.get("actor")
@@ -500,106 +385,64 @@ class UserTimeline(BaseBuffer):
def start_stream(self, mandatory=False, play_sound=True):
if not self.actor:
return 0
count = 50
try:
count = self.session.settings["general"].get("max_posts_per_call", 50)
except Exception:
pass
actor = self.actor
if isinstance(actor, str):
actor = actor.strip()
if actor.startswith("@"):
actor = actor[1:]
count = self.get_max_items()
actor = self.actor.strip().lstrip("@") if isinstance(self.actor, str) else self.actor
api = self.session._ensure_client()
if not api:
return 0
try:
if isinstance(actor, str) and not actor.startswith("did:"):
try:
profile = self.session.get_profile(actor)
if profile:
def g(obj, key, default=None):
if isinstance(obj, dict):
return obj.get(key, default)
return getattr(obj, key, default)
did = g(profile, "did")
if did:
actor = did
except Exception:
pass
profile = self.session.get_profile(actor)
if profile:
did = profile.get("did") if isinstance(profile, dict) else getattr(profile, "did", None)
if did:
actor = did
self._resolved_actor = actor
res = api.app.bsky.feed.get_author_feed({
"actor": actor,
"limit": count,
})
items = getattr(res, "feed", []) or []
res = api.app.bsky.feed.get_author_feed({"actor": actor, "limit": count})
items = list(getattr(res, "feed", []) or [])
self.next_cursor = getattr(res, "cursor", None)
except Exception:
log.exception("Error fetching user timeline")
except Exception as e:
log.error("Error fetching user timeline: %s", e)
return 0
return self.process_items(list(items), play_sound)
return self.process_items(items, play_sound)
def get_more_items(self):
if not self.next_cursor or not self._resolved_actor:
return
count = 50
try:
count = self.session.settings["general"].get("max_posts_per_call", 50)
except:
pass
count = self.get_max_items()
api = self.session._ensure_client()
if not api:
return
try:
res = api.app.bsky.feed.get_author_feed({
"actor": self._resolved_actor,
"limit": count,
"cursor": self.next_cursor
})
items = getattr(res, "feed", []) or []
res = api.app.bsky.feed.get_author_feed({"actor": self._resolved_actor, "limit": count, "cursor": self.next_cursor})
items = list(getattr(res, "feed", []) or [])
self.next_cursor = getattr(res, "cursor", None)
added = self.process_items(list(items), play_sound=False)
added = self.process_items(items, play_sound=False)
if added:
output.speak(_(u"%s items retrieved") % (str(added)), True)
except Exception:
log.exception("Error fetching more user timeline items")
output.speak(_(u"%s items retrieved") % added, True)
except Exception as e:
log.error("Error fetching more user timeline: %s", e)
def remove_buffer(self, force=False):
if not force:
from wxUI import commonMessageDialogs
import widgetUtils
dlg = commonMessageDialogs.remove_buffer()
if dlg != widgetUtils.YES:
if commonMessageDialogs.remove_buffer() != widgetUtils.YES:
return False
try:
self.session.db.pop(self.name, None)
except Exception:
pass
try:
timelines = self.session.settings["other_buffers"].get("timelines")
if timelines is None:
timelines = []
if isinstance(timelines, str):
timelines = [t for t in timelines.split(",") if t]
actor = self.actor or ""
handle = self.handle or ""
for key in (actor, handle):
if key in timelines:
timelines.remove(key)
self.session.settings["other_buffers"]["timelines"] = timelines
self.session.settings.write()
except Exception:
log.exception("Error updating Bluesky timelines settings")
self.session.db.pop(self.name, None)
timelines = self.session.settings["other_buffers"].get("timelines") or []
if isinstance(timelines, str):
timelines = [t for t in timelines.split(",") if t]
for key in (self.actor or "", self.handle or ""):
if key in timelines:
timelines.remove(key)
self.session.settings["other_buffers"]["timelines"] = timelines
self.session.settings.write()
return True
class SearchBuffer(BaseBuffer):
"""Buffer for search results (posts)."""
"""Search results buffer."""
def __init__(self, *args, **kwargs):
self.search_query = kwargs.pop("query", "")
@@ -615,86 +458,52 @@ class SearchBuffer(BaseBuffer):
def start_stream(self, mandatory=False, play_sound=True):
if not self.search_query:
return 0
count = 50
try:
count = self.session.settings["general"].get("max_posts_per_call", 50)
except Exception:
pass
count = self.get_max_items()
api = self.session._ensure_client()
if not api:
return 0
try:
# Search posts
res = api.app.bsky.feed.search_posts({
"q": self.search_query,
"limit": count
})
posts = getattr(res, "posts", [])
res = api.app.bsky.feed.search_posts({"q": self.search_query, "limit": count})
posts = list(getattr(res, "posts", []))
self.next_cursor = getattr(res, "cursor", None)
if not posts:
return 0
# Clear existing results for new search
self.session.db[self.name] = []
self.buffer.list.clear()
return self.process_items(list(posts), play_sound)
except Exception:
log.exception("Error searching Bluesky posts")
return self.process_items(posts, play_sound)
except Exception as e:
log.error("Error searching posts: %s", e)
return 0
def get_more_items(self):
if not self.next_cursor or not self.search_query:
return
count = 50
try:
count = self.session.settings["general"].get("max_posts_per_call", 50)
except:
pass
count = self.get_max_items()
api = self.session._ensure_client()
if not api:
return
try:
res = api.app.bsky.feed.search_posts({
"q": self.search_query,
"limit": count,
"cursor": self.next_cursor
})
posts = getattr(res, "posts", [])
res = api.app.bsky.feed.search_posts({"q": self.search_query, "limit": count, "cursor": self.next_cursor})
posts = list(getattr(res, "posts", []))
self.next_cursor = getattr(res, "cursor", None)
added = self.process_items(list(posts), play_sound=False)
added = self.process_items(posts, play_sound=False)
if added:
output.speak(_(u"%s items retrieved") % (str(added)), True)
except Exception:
log.exception("Error fetching more search results")
output.speak(_(u"%s items retrieved") % added, True)
except Exception as e:
log.error("Error fetching more search results: %s", e)
def remove_buffer(self, force=False):
"""Search buffers can always be removed."""
if not force:
from wxUI import commonMessageDialogs
import widgetUtils
dlg = commonMessageDialogs.remove_buffer()
if dlg != widgetUtils.YES:
if commonMessageDialogs.remove_buffer() != widgetUtils.YES:
return False
try:
self.session.db.pop(self.name, None)
except Exception:
pass
# Also remove from saved searches
try:
searches = self.session.settings["other_buffers"].get("searches")
if searches:
if isinstance(searches, str):
searches = [s for s in searches.split(",") if s]
if self.search_query in searches:
searches.remove(self.search_query)
self.session.settings["other_buffers"]["searches"] = searches
self.session.settings.write()
except Exception:
log.exception("Error updating saved searches")
self.session.db.pop(self.name, None)
searches = self.session.settings["other_buffers"].get("searches") or []
if isinstance(searches, str):
searches = [s for s in searches.split(",") if s]
if self.search_query in searches:
searches.remove(self.search_query)
self.session.settings["other_buffers"]["searches"] = searches
self.session.settings.write()
return True

View File

@@ -24,7 +24,7 @@ class UserBuffer(BaseBuffer):
api_method = self.kwargs.get("api_method")
if not api_method: return 0
count = self.session.settings["general"].get("max_posts_per_call", 50)
count = self.get_max_items()
actor = (
self.kwargs.get("actor")
or self.kwargs.get("did")
@@ -33,22 +33,15 @@ class UserBuffer(BaseBuffer):
)
try:
# We call the method in session. API methods return {"items": [...], "cursor": ...}
if api_method in ("get_followers", "get_follows"):
res = getattr(self.session, api_method)(actor=actor, limit=count)
else:
res = getattr(self.session, api_method)(limit=count)
items = res.get("items", [])
self.next_cursor = res.get("cursor")
# Clear existing items for these lists to start fresh?
# Or append? Standard lists in TWBlue usually append.
# But followers/blocks are often full-sync or large jumps.
# For now, append like timelines.
return self.process_items(items, play_sound)
except Exception:
log.exception(f"Error fetching user list for {self.name}")
except Exception as e:
log.error("Error fetching user list for %s: %s", self.name, e)
return 0
def get_more_items(self):
@@ -56,7 +49,7 @@ class UserBuffer(BaseBuffer):
if not api_method or not self.next_cursor:
return
count = self.session.settings["general"].get("max_posts_per_call", 50)
count = self.get_max_items()
actor = (
self.kwargs.get("actor")
or self.kwargs.get("did")
@@ -73,8 +66,8 @@ class UserBuffer(BaseBuffer):
added = self.process_items(items, play_sound=False)
if added:
output.speak(_(u"%s items retrieved") % (str(added)), True)
except Exception:
log.exception(f"Error fetching more user list items for {self.name}")
except Exception as e:
log.error("Error fetching more user list items for %s: %s", self.name, e)
class FollowersBuffer(UserBuffer):
def __init__(self, *args, **kwargs):
@@ -102,8 +95,8 @@ class FollowersBuffer(UserBuffer):
timelines.remove(key)
self.session.settings["other_buffers"]["followers_timelines"] = timelines
self.session.settings.write()
except Exception:
log.exception("Error updating Bluesky followers timelines settings")
except Exception as e:
log.error("Error updating Bluesky followers timelines settings: %s", e)
return True
class FollowingBuffer(UserBuffer):
@@ -132,8 +125,8 @@ class FollowingBuffer(UserBuffer):
timelines.remove(key)
self.session.settings["other_buffers"]["following_timelines"] = timelines
self.session.settings.write()
except Exception:
log.exception("Error updating Bluesky following timelines settings")
except Exception as e:
log.error("Error updating Bluesky following timelines settings: %s", e)
return True
class BlocksBuffer(UserBuffer):
@@ -152,20 +145,20 @@ class PostUserListBuffer(UserBuffer):
def start_stream(self, mandatory=False, play_sound=True):
if not self.api_method or not self.post_uri:
return 0
count = self.session.settings["general"].get("max_posts_per_call", 50)
count = self.get_max_items()
try:
res = getattr(self.session, self.api_method)(self.post_uri, limit=count)
items = res.get("items", [])
self.next_cursor = res.get("cursor")
return self.process_items(items, play_sound)
except Exception:
log.exception("Error fetching post user list for %s", self.name)
except Exception as e:
log.error("Error fetching post user list for %s: %s", self.name, e)
return 0
def get_more_items(self):
if not self.api_method or not self.post_uri or not self.next_cursor:
return
count = self.session.settings["general"].get("max_posts_per_call", 50)
count = self.get_max_items()
try:
res = getattr(self.session, self.api_method)(self.post_uri, limit=count, cursor=self.next_cursor)
items = res.get("items", [])
@@ -173,8 +166,8 @@ class PostUserListBuffer(UserBuffer):
added = self.process_items(items, play_sound=False)
if added:
output.speak(_(u"%s items retrieved") % (str(added)), True)
except Exception:
log.exception("Error fetching more post user list items for %s", self.name)
except Exception as e:
log.error("Error fetching more post user list items for %s: %s", self.name, e)
def remove_buffer(self, force=False):
if not force: