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

View File

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

View File

@@ -20,19 +20,15 @@ class ConversationListBuffer(BaseBuffer):
self.buffer.session = self.session self.buffer.session = self.session
def start_stream(self, mandatory=False, play_sound=True): 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: try:
res = self.session.list_convos(limit=count) res = self.session.list_convos(limit=count)
items = res.get("items", []) items = res.get("items", [])
# Clear to avoid list weirdness on refreshes?
# Chat list usually replaces content on fetch
self.session.db[self.name] = [] self.session.db[self.name] = []
self.buffer.list.clear() self.buffer.list.clear()
return self.process_items(items, play_sound) return self.process_items(items, play_sound)
except Exception: except Exception as e:
log.exception("Error fetching conversations") log.error("Error fetching conversations: %s", e)
return 0 return 0
def url(self, *args, **kwargs): def url(self, *args, **kwargs):
@@ -80,23 +76,18 @@ class ChatBuffer(BaseBuffer):
self.buffer.session = self.session self.buffer.session = self.session
def start_stream(self, mandatory=False, play_sound=True): def start_stream(self, mandatory=False, play_sound=True):
if not self.convo_id: return 0 if not self.convo_id:
count = self.session.settings["general"].get("max_posts_per_call", 50) return 0
count = self.get_max_items()
try: try:
res = self.session.get_convo_messages(self.convo_id, limit=count) res = self.session.get_convo_messages(self.convo_id, limit=count)
items = res.get("items", []) 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.session.db[self.name] = []
self.buffer.list.clear() self.buffer.list.clear()
# API usually returns newest first. We want newest at bottom.
items = list(reversed(items)) items = list(reversed(items))
return self.process_items(items, play_sound) return self.process_items(items, play_sound)
except Exception: except Exception as e:
log.exception("Error fetching chat messages") log.error("Error fetching chat messages: %s", e)
return 0 return 0
def on_reply(self, evt): def on_reply(self, evt):

View File

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

View File

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