mirror of
https://github.com/MCV-Software/TWBlue.git
synced 2026-03-07 01:47:32 +01:00
Terminando integración
This commit is contained in:
@@ -1,4 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from .timeline import HomeTimeline, FollowingTimeline, NotificationBuffer, Conversation
|
||||
from .timeline import (
|
||||
HomeTimeline,
|
||||
FollowingTimeline,
|
||||
NotificationBuffer,
|
||||
Conversation,
|
||||
LikesBuffer,
|
||||
MentionsBuffer,
|
||||
SentBuffer,
|
||||
SearchBuffer,
|
||||
)
|
||||
from .user import FollowersBuffer, FollowingBuffer, BlocksBuffer
|
||||
from .chat import ConversationListBuffer, ChatBuffer as ChatMessageBuffer
|
||||
|
||||
@@ -123,20 +123,84 @@ class BaseBuffer(base.Buffer):
|
||||
output.speak(_("Reposted."))
|
||||
|
||||
def on_like(self, evt):
|
||||
self.toggle_favorite(confirm=True)
|
||||
self.toggle_favorite(confirm=False)
|
||||
|
||||
def toggle_favorite(self, confirm=False, *args, **kwargs):
|
||||
item = self.get_item()
|
||||
if not item: return
|
||||
uri = item.get("uri") if isinstance(item, dict) else getattr(item, "uri", None)
|
||||
if not item:
|
||||
output.speak(_("No item to like."), True)
|
||||
return
|
||||
|
||||
def g(obj, key, default=None):
|
||||
if isinstance(obj, dict):
|
||||
return obj.get(key, default)
|
||||
return getattr(obj, key, default)
|
||||
|
||||
uri = g(item, "uri")
|
||||
if not uri:
|
||||
post = g(item, "post") or g(item, "record")
|
||||
uri = g(post, "uri") if post else None
|
||||
|
||||
if not uri:
|
||||
output.speak(_("Could not find post identifier."), True)
|
||||
return
|
||||
|
||||
if confirm:
|
||||
if wx.MessageBox(_("Like this post?"), _("Confirm"), wx.YES_NO | wx.ICON_QUESTION) != wx.YES:
|
||||
return
|
||||
|
||||
self.session.like(uri)
|
||||
# Check if already liked
|
||||
viewer = g(item, "viewer")
|
||||
already_liked = g(viewer, "like") if viewer else None
|
||||
|
||||
if already_liked:
|
||||
output.speak(_("Already liked."), True)
|
||||
return
|
||||
|
||||
# Perform the like
|
||||
like_uri = self.session.like(uri)
|
||||
if not like_uri:
|
||||
output.speak(_("Failed to like post."), True)
|
||||
return
|
||||
|
||||
output.speak(_("Liked."))
|
||||
|
||||
# Update the viewer state in the item
|
||||
if isinstance(item, dict):
|
||||
if "viewer" not in item:
|
||||
item["viewer"] = {}
|
||||
item["viewer"]["like"] = like_uri
|
||||
else:
|
||||
# For SDK models, create or update viewer
|
||||
if not hasattr(item, "viewer") or item.viewer is None:
|
||||
# Create a simple object to hold the like state
|
||||
class Viewer:
|
||||
def __init__(self):
|
||||
self.like = None
|
||||
item.viewer = Viewer()
|
||||
item.viewer.like = like_uri
|
||||
|
||||
# Refresh the displayed item in the list
|
||||
try:
|
||||
index = self.buffer.list.get_selected()
|
||||
if index > -1:
|
||||
# Recompose and update the list item
|
||||
safe = True
|
||||
relative_times = self.session.settings["general"].get("relative_times", False)
|
||||
show_screen_names = self.session.settings["general"].get("show_screen_names", False)
|
||||
post_data = self.compose_function(item, self.session.db, self.session.settings,
|
||||
relative_times=relative_times,
|
||||
show_screen_names=show_screen_names,
|
||||
safe=safe)
|
||||
|
||||
# Update the item in place (only 3 columns: Author, Post, Date)
|
||||
self.buffer.list.list.SetItem(index, 0, post_data[0]) # Author
|
||||
self.buffer.list.list.SetItem(index, 1, post_data[1]) # Text (with ♥ indicator)
|
||||
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")
|
||||
|
||||
def add_to_favorites(self, *args, **kwargs):
|
||||
self.toggle_favorite(confirm=False)
|
||||
|
||||
@@ -172,8 +236,9 @@ class BaseBuffer(base.Buffer):
|
||||
if text:
|
||||
try:
|
||||
api = self.session._ensure_client()
|
||||
dm_client = api.with_bsky_chat_proxy()
|
||||
# Get or create conversation
|
||||
res = api.chat.bsky.convo.get_convo_for_members({"members": [did]})
|
||||
res = dm_client.chat.bsky.convo.get_convo_for_members({"members": [did]})
|
||||
convo_id = res.convo.id
|
||||
self.session.send_chat_message(convo_id, text)
|
||||
output.speak(_("Message sent."), True)
|
||||
@@ -186,6 +251,46 @@ class BaseBuffer(base.Buffer):
|
||||
# If showing, we'll just open the chat buffer for now as it's more structured
|
||||
self.view_chat_with_user(did, handle)
|
||||
|
||||
def url(self, *args, **kwargs):
|
||||
item = self.get_item()
|
||||
if not item: return
|
||||
|
||||
import webbrowser
|
||||
|
||||
def g(obj, key, default=None):
|
||||
if isinstance(obj, dict):
|
||||
return obj.get(key, default)
|
||||
return getattr(obj, key, default)
|
||||
|
||||
uri = g(item, "uri")
|
||||
author = g(item, "author") or g(g(item, "post"), "author")
|
||||
handle = g(author, "handle")
|
||||
|
||||
if uri and handle:
|
||||
# URI format: at://did:plc:xxx/app.bsky.feed.post/rkey
|
||||
if "app.bsky.feed.post" in uri:
|
||||
rkey = uri.split("/")[-1]
|
||||
url = f"https://bsky.app/profile/{handle}/post/{rkey}"
|
||||
webbrowser.open(url)
|
||||
return
|
||||
elif "app.bsky.feed.like" in uri:
|
||||
# It's a like notification, try to get the subject
|
||||
subject = g(item, "subject")
|
||||
subject_uri = g(subject, "uri") if subject else None
|
||||
if subject_uri:
|
||||
rkey = subject_uri.split("/")[-1]
|
||||
# We might not have the handle of the post author here easily if it's not in the notification
|
||||
# But let's try...
|
||||
# Actually, notification items usually have enough info or we can't deep direct link easily without fetching.
|
||||
# For now, let's just open the profile of the liker
|
||||
pass
|
||||
|
||||
# Fallback to profile
|
||||
if handle:
|
||||
url = f"https://bsky.app/profile/{handle}"
|
||||
webbrowser.open(url)
|
||||
return
|
||||
|
||||
def user_actions(self, *args, **kwargs):
|
||||
pub.sendMessage("execute-action", action="follow")
|
||||
|
||||
|
||||
@@ -100,28 +100,43 @@ class FollowingTimeline(BaseBuffer):
|
||||
|
||||
class NotificationBuffer(BaseBuffer):
|
||||
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"
|
||||
|
||||
self.sound = "notification_received.ogg"
|
||||
|
||||
def create_buffer(self, parent, name):
|
||||
self.buffer = BlueskiPanels.NotificationPanel(parent, name)
|
||||
self.buffer = BlueskiPanels.NotificationPanel(parent, name)
|
||||
self.buffer.session = self.session
|
||||
|
||||
def start_stream(self, mandatory=False, play_sound=True):
|
||||
count = 50
|
||||
api = self.session._ensure_client()
|
||||
try:
|
||||
res = api.app.bsky.notification.list_notifications({"limit": count})
|
||||
notifs = getattr(res, "notifications", [])
|
||||
items = []
|
||||
# Notifications are not FeedViewPost. They have different structure.
|
||||
# self.compose_function expects FeedViewPost-like structure (post, author, etc).
|
||||
# We need to map them or have a different compose function.
|
||||
# For now, let's skip items to avoid crash
|
||||
# Or attempt to map.
|
||||
except:
|
||||
return 0
|
||||
return 0
|
||||
count = 50
|
||||
try:
|
||||
count = self.session.settings["general"].get("max_posts_per_call", 50)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
api = self.session._ensure_client()
|
||||
if not api:
|
||||
return 0
|
||||
|
||||
try:
|
||||
res = api.app.bsky.notification.list_notifications({"limit": count})
|
||||
notifications = getattr(res, "notifications", [])
|
||||
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 0
|
||||
|
||||
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):
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -207,3 +222,154 @@ class LikesBuffer(BaseBuffer):
|
||||
return 0
|
||||
|
||||
return self.process_items(list(items), play_sound)
|
||||
|
||||
|
||||
class MentionsBuffer(BaseBuffer):
|
||||
"""Buffer for mentions and replies to the current user."""
|
||||
|
||||
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"
|
||||
self.sound = "mention_received.ogg"
|
||||
|
||||
def create_buffer(self, parent, name):
|
||||
self.buffer = BlueskiPanels.NotificationPanel(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
|
||||
|
||||
api = self.session._ensure_client()
|
||||
if not api:
|
||||
return 0
|
||||
|
||||
try:
|
||||
res = api.app.bsky.notification.list_notifications({"limit": count})
|
||||
notifications = getattr(res, "notifications", [])
|
||||
if not notifications:
|
||||
return 0
|
||||
|
||||
# Filter only mentions and replies
|
||||
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")
|
||||
return 0
|
||||
|
||||
def add_new_item(self, notification):
|
||||
"""Add a single new mention from streaming/polling."""
|
||||
reason = getattr(notification, "reason", "")
|
||||
if 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."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SentBuffer, self).__init__(*args, **kwargs)
|
||||
self.type = "sent"
|
||||
|
||||
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
|
||||
|
||||
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", [])
|
||||
|
||||
if not items:
|
||||
return 0
|
||||
|
||||
return self.process_items(list(items), play_sound)
|
||||
|
||||
except Exception:
|
||||
log.exception("Error fetching sent posts")
|
||||
return 0
|
||||
|
||||
|
||||
class SearchBuffer(BaseBuffer):
|
||||
"""Buffer for search results (posts)."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.search_query = kwargs.pop("query", "")
|
||||
super(SearchBuffer, self).__init__(*args, **kwargs)
|
||||
self.type = "search"
|
||||
|
||||
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):
|
||||
if not self.search_query:
|
||||
return 0
|
||||
|
||||
count = 50
|
||||
try:
|
||||
count = self.session.settings["general"].get("max_posts_per_call", 50)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
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", [])
|
||||
|
||||
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 0
|
||||
|
||||
def remove_buffer(self, force=False):
|
||||
"""Search buffers can always be removed."""
|
||||
try:
|
||||
self.session.db.pop(self.name, None)
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user