mirror of
https://github.com/MCV-Software/TWBlue.git
synced 2026-03-07 09:57:32 +01:00
Commit
This commit is contained in:
3
src/controller/blueski/__init__.py
Normal file
3
src/controller/blueski/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .handler import Handler
|
||||
|
||||
__all__ = ["Handler"]
|
||||
99
src/controller/blueski/handler.py
Normal file
99
src/controller/blueski/handler.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
import languageHandler # Ensure _() injection
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Handler:
|
||||
"""Handler for Bluesky integration: creates minimal buffers."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.menus = dict(
|
||||
compose="&Post",
|
||||
)
|
||||
self.item_menu = "&Post"
|
||||
|
||||
def create_buffers(self, session, createAccounts=True, controller=None):
|
||||
name = session.get_name()
|
||||
controller.accounts.append(name)
|
||||
if createAccounts:
|
||||
from pubsub import pub
|
||||
pub.sendMessage("core.create_account", name=name, session_id=session.session_id, logged=True)
|
||||
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)
|
||||
)
|
||||
# Following-only timeline (reverse-chronological)
|
||||
pub.sendMessage(
|
||||
"createBuffer",
|
||||
buffer_type="following_timeline",
|
||||
session_type="blueski",
|
||||
buffer_title=_("Following (Chronological)"),
|
||||
parent_tab=root_position,
|
||||
start=False,
|
||||
kwargs=dict(parent=controller.view.nb, name="following_timeline", session=session)
|
||||
)
|
||||
|
||||
def start_buffer(self, controller, buffer):
|
||||
"""Start a newly created Bluesky buffer."""
|
||||
try:
|
||||
if hasattr(buffer, "start_stream"):
|
||||
buffer.start_stream(mandatory=True, play_sound=False)
|
||||
# Enable periodic auto-refresh to simulate real-time updates
|
||||
if hasattr(buffer, "enable_auto_refresh"):
|
||||
buffer.enable_auto_refresh()
|
||||
finally:
|
||||
# Ensure we won't try to start it again
|
||||
try:
|
||||
buffer.needs_init = False
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def account_settings(self, buffer, controller):
|
||||
"""Open a minimal account settings dialog for Bluesky."""
|
||||
try:
|
||||
current_mode = None
|
||||
try:
|
||||
current_mode = buffer.session.settings["general"].get("boost_mode")
|
||||
except Exception:
|
||||
current_mode = None
|
||||
ask_default = True if current_mode in (None, "ask") else False
|
||||
|
||||
from wxUI.dialogs.blueski.configuration import AccountSettingsDialog
|
||||
dlg = AccountSettingsDialog(controller.view, ask_before_boost=ask_default)
|
||||
resp = dlg.ShowModal()
|
||||
if resp == wx.ID_OK:
|
||||
vals = dlg.get_values()
|
||||
boost_mode = "ask" if vals.get("ask_before_boost") else "direct"
|
||||
try:
|
||||
buffer.session.settings["general"]["boost_mode"] = boost_mode
|
||||
buffer.session.settings.write()
|
||||
except Exception:
|
||||
logger.exception("Failed to persist Bluesky boost_mode setting")
|
||||
dlg.Destroy()
|
||||
except Exception:
|
||||
logger.exception("Error opening Bluesky account settings dialog")
|
||||
|
||||
async def handle_action(self, action_name: str, user_id: str, payload: dict[str, Any]) -> dict[str, Any] | None:
|
||||
logger.debug("handle_action stub: %s %s %s", action_name, user_id, payload)
|
||||
return None
|
||||
|
||||
async def handle_message_command(self, command: str, user_id: str, message_id: str, payload: dict[str, Any]) -> dict[str, Any] | None:
|
||||
logger.debug("handle_message_command stub: %s %s %s %s", command, user_id, message_id, payload)
|
||||
return None
|
||||
|
||||
async def handle_user_command(self, command: str, user_id: str, target_user_id: str, payload: dict[str, Any]) -> dict[str, Any] | None:
|
||||
logger.debug("handle_user_command stub: %s %s %s %s", command, user_id, target_user_id, payload)
|
||||
return None
|
||||
91
src/controller/blueski/messages.py
Normal file
91
src/controller/blueski/messages.py
Normal file
@@ -0,0 +1,91 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
# Translation function is provided globally by TWBlue's language handler (_)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# This file would typically contain functions to generate complex message bodies or
|
||||
# interactive components for Blueski, similar to how it might be done for Mastodon.
|
||||
# Since Blueski's interactive features (beyond basic posts) are still evolving
|
||||
# or client-dependent (like polls), this might be less complex initially.
|
||||
|
||||
# Example: If Blueski develops a standard for "cards" or interactive messages,
|
||||
# functions to create those would go here. For now, we can imagine placeholders.
|
||||
|
||||
def format_welcome_message(session: Any) -> dict[str, Any]:
|
||||
"""
|
||||
Generates a welcome message for a new Blueski session.
|
||||
This is just a placeholder and example.
|
||||
"""
|
||||
# user_profile = session.util.get_own_profile_info() # Assuming this method exists and is async or cached
|
||||
# handle = user_profile.get("handle", _("your Blueski account")) if user_profile else _("your Blueski account")
|
||||
# Expect session to expose username via db/settings
|
||||
handle = (getattr(session, "db", {}).get("user_name")
|
||||
or getattr(getattr(session, "settings", {}), "get", lambda *_: {})("blueski").get("handle")
|
||||
or _("your Bluesky account"))
|
||||
|
||||
|
||||
return {
|
||||
"text": _("Welcome to Approve for Blueski! Your account {handle} is connected.").format(handle=handle),
|
||||
# "blocks": [ # If Blueski supports a block kit like Slack or Discord
|
||||
# {
|
||||
# "type": "section",
|
||||
# "text": {
|
||||
# "type": "mrkdwn", # Or Blueski's equivalent
|
||||
# "text": _("Welcome to Approve for Blueski! Your account *{handle}* is connected.").format(handle=handle)
|
||||
# }
|
||||
# },
|
||||
# {
|
||||
# "type": "actions",
|
||||
# "elements": [
|
||||
# {
|
||||
# "type": "button",
|
||||
# "text": {"type": "plain_text", "text": _("Post your first Skeet")},
|
||||
# "action_id": "blueski_compose_new_post" # Example action ID
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
# ]
|
||||
}
|
||||
|
||||
def format_error_message(error_description: str, details: str | None = None) -> dict[str, Any]:
|
||||
"""
|
||||
Generates a standardized error message.
|
||||
"""
|
||||
message = {"text": f":warning: Error: {error_description}"} # Basic text message
|
||||
# if details:
|
||||
# message["blocks"] = [
|
||||
# {
|
||||
# "type": "section",
|
||||
# "text": {"type": "mrkdwn", "text": f":warning: *Error:* {error_description}\n{details}"}
|
||||
# }
|
||||
# ]
|
||||
return message
|
||||
|
||||
# More functions could be added here as Blueski's capabilities become clearer
|
||||
# or as specific formatting needs for Approve arise. For example:
|
||||
# - Formatting a post for display with all its embeds and cards.
|
||||
# - Generating help messages specific to Blueski features.
|
||||
# - Creating interactive messages for polls (if supported via some convention).
|
||||
|
||||
# Example of adapting a function that might exist in mastodon_messages:
|
||||
# def build_post_summary_message(session: BlueskiSession, post_uri: str, post_content: dict) -> dict[str, Any]:
|
||||
# """
|
||||
# Builds a summary message for an Blueski post.
|
||||
# """
|
||||
# author_handle = post_content.get("author", {}).get("handle", "Unknown user")
|
||||
# text_preview = post_content.get("text", "")[:100] # First 100 chars of text
|
||||
# # url = session.get_message_url(post_uri) # Assuming this method exists
|
||||
# url = f"https://bsky.app/profile/{author_handle}/post/{post_uri.split('/')[-1]}" # Construct a URL
|
||||
|
||||
# return {
|
||||
# "text": _("Post by {author_handle}: {text_preview}... ({url})").format(
|
||||
# author_handle=author_handle, text_preview=text_preview, url=url
|
||||
# ),
|
||||
# # Potentially with "blocks" for richer formatting if the platform supports it
|
||||
# }
|
||||
|
||||
logger.info("Blueski messages module loaded (placeholders).")
|
||||
128
src/controller/blueski/settings.py
Normal file
128
src/controller/blueski/settings.py
Normal file
@@ -0,0 +1,128 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
fromapprove.forms import Form, SubmitField, TextAreaField, TextField
|
||||
fromapprove.translation import translate as _
|
||||
|
||||
if TYPE_CHECKING:
|
||||
fromapprove.config import ConfigSectionProxy
|
||||
fromapprove.sessions.blueski.session import Session as BlueskiSession # Adjusted
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# This file is for defining forms and handling for Blueski-specific settings
|
||||
# that might be more complex than simple key-value pairs handled by Session.get_settings_inputs.
|
||||
# For Blueski, initial settings might be simple (handle, app password),
|
||||
# but this structure allows for expansion.
|
||||
|
||||
|
||||
class BlueskiSettingsForm(Form):
|
||||
"""
|
||||
A settings form for Blueski sessions.
|
||||
This would mirror the kind of settings found in Session.get_settings_inputs
|
||||
but using the WTForms-like Form structure for more complex validation or layout.
|
||||
"""
|
||||
# Example fields - these should align with what BlueskiSession.get_settings_inputs defines
|
||||
# and what BlueskiSession.get_configurable_values expects for its config.
|
||||
|
||||
# instance_url = TextField(
|
||||
# _("Instance URL"),
|
||||
# default="https://bsky.social", # Default PDS for Bluesky
|
||||
# description=_("The base URL of your Blueski PDS instance (e.g., https://bsky.social)."),
|
||||
# validators=[], # Add validators if needed, e.g., URL validator
|
||||
# )
|
||||
handle = TextField(
|
||||
_("Bluesky Handle"),
|
||||
description=_("Your Bluesky user handle (e.g., @username.bsky.social or username.bsky.social)."),
|
||||
validators=[], # e.g., DataRequired()
|
||||
)
|
||||
app_password = TextField( # Consider PasswordField if sensitive and your Form class supports it
|
||||
_("App Password"),
|
||||
description=_("Your Bluesky App Password. Generate this in your Bluesky account settings."),
|
||||
validators=[], # e.g., DataRequired()
|
||||
)
|
||||
# Add more fields as needed for Blueski configuration.
|
||||
# For example, if there were specific notification settings, content filters, etc.
|
||||
|
||||
submit = SubmitField(_("Save Blueski Settings"))
|
||||
|
||||
|
||||
async def get_settings_form(
|
||||
user_id: str,
|
||||
session: BlueskiSession | None = None,
|
||||
config: ConfigSectionProxy | None = None, # User-specific config for Blueski
|
||||
) -> BlueskiSettingsForm:
|
||||
"""
|
||||
Creates and pre-populates the Blueski settings form.
|
||||
"""
|
||||
form_data = {}
|
||||
if session: # If a session exists, use its current config
|
||||
# form_data["instance_url"] = session.config_get("api_base_url", "https://bsky.social")
|
||||
form_data["handle"] = session.config_get("handle", "")
|
||||
# App password should not be pre-filled for security.
|
||||
form_data["app_password"] = ""
|
||||
elif config: # Fallback to persisted config if no active session
|
||||
# form_data["instance_url"] = config.api_base_url.get("https://bsky.social")
|
||||
form_data["handle"] = config.handle.get("")
|
||||
form_data["app_password"] = ""
|
||||
|
||||
form = BlueskiSettingsForm(formdata=None, **form_data) # formdata=None for initial display
|
||||
return form
|
||||
|
||||
|
||||
async def process_settings_form(
|
||||
form: BlueskiSettingsForm,
|
||||
user_id: str,
|
||||
session: BlueskiSession | None = None, # Pass if update should affect live session
|
||||
config: ConfigSectionProxy | None = None, # User-specific config for Blueski
|
||||
) -> bool:
|
||||
"""
|
||||
Processes the submitted Blueski settings form and updates configuration.
|
||||
Returns True if successful, False otherwise.
|
||||
"""
|
||||
if not form.validate(): # Assuming form has a validate method
|
||||
logger.warning(f"Blueski settings form validation failed for user {user_id}: {form.errors}")
|
||||
return False
|
||||
|
||||
if not config and session: # Try to get config via session if not directly provided
|
||||
# This depends on how ConfigSectionProxy is obtained.
|
||||
# config = approve.config.config.sessions.blueski[user_id] # Example path
|
||||
pass # Needs actual way to get config proxy
|
||||
|
||||
if not config:
|
||||
logger.error(f"Cannot process Blueski settings for user {user_id}: no config proxy available.")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Update the configuration values
|
||||
# await config.api_base_url.set(form.instance_url.data)
|
||||
await config.handle.set(form.handle.data)
|
||||
await config.app_password.set(form.app_password.data) # Ensure this is stored securely
|
||||
|
||||
logger.info(f"Blueski settings updated for user {user_id}.")
|
||||
|
||||
# If there's an active session, it might need to be reconfigured or restarted
|
||||
if session:
|
||||
logger.info(f"Requesting Blueski session re-initialization for user {user_id} due to settings change.")
|
||||
# await session.stop() # Stop it
|
||||
# # Update session instance with new values directly or rely on it re-reading config
|
||||
# session.api_base_url = form.instance_url.data
|
||||
# session.handle = form.handle.data
|
||||
# # App password should be handled carefully, session might need to re-login
|
||||
# await session.start() # Restart with new settings
|
||||
# Or, more simply, the session might have a reconfigure method:
|
||||
# await session.reconfigure(new_settings_dict)
|
||||
pass # Placeholder for session reconfiguration logic
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving Blueski settings for user {user_id}: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
# Any additional Blueski-specific settings views or handlers would go here.
|
||||
# For instance, if Blueski had features like "Relays" or "Feed Generators"
|
||||
# that needed UI configuration within Approve, those forms and handlers could be defined here.
|
||||
|
||||
logger.info("Blueski settings module loaded (placeholders).")
|
||||
153
src/controller/blueski/templateEditor.py
Normal file
153
src/controller/blueski/templateEditor.py
Normal file
@@ -0,0 +1,153 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
# fromapprove.controller.mastodon import templateEditor as mastodon_template_editor # If adapting
|
||||
fromapprove.translation import translate as _
|
||||
|
||||
if TYPE_CHECKING:
|
||||
fromapprove.sessions.blueski.session import Session as BlueskiSession # Adjusted
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# This file would handle the logic for a template editor specific to Blueski.
|
||||
# A template editor allows users to customize how certain information or messages
|
||||
# from Blueski are displayed in Approve.
|
||||
|
||||
# For Blueski, this might be less relevant initially if its content structure
|
||||
# is simpler than Mastodon's, or if user-customizable templates are not a primary feature.
|
||||
# However, having the structure allows for future expansion.
|
||||
|
||||
# Example: Customizing the format of a "new follower" notification, or how a "skeet" is displayed.
|
||||
|
||||
class BlueskiTemplateEditor:
|
||||
def __init__(self, session: BlueskiSession) -> None:
|
||||
self.session = session
|
||||
# self.user_id = session.user_id
|
||||
# self.config_prefix = f"sessions.blueski.{self.user_id}.templates." # Example config path
|
||||
|
||||
def get_editable_templates(self) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Returns a list of templates that the user can edit for Blueski.
|
||||
Each entry should describe the template, its purpose, and current value.
|
||||
"""
|
||||
# This would typically fetch template definitions from a default set
|
||||
# and override with any user-customized versions from config.
|
||||
|
||||
# Example structure for an editable template:
|
||||
# templates = [
|
||||
# {
|
||||
# "id": "new_follower_notification", # Unique ID for this template
|
||||
# "name": _("New Follower Notification Format"),
|
||||
# "description": _("Customize how new follower notifications from Blueski are displayed."),
|
||||
# "default_template": "{{ actor.displayName }} (@{{ actor.handle }}) is now following you on Blueski!",
|
||||
# "current_template": self._get_template_content("new_follower_notification"),
|
||||
# "variables": [ # Available variables for this template
|
||||
# {"name": "actor.displayName", "description": _("Display name of the new follower")},
|
||||
# {"name": "actor.handle", "description": _("Handle of the new follower")},
|
||||
# {"name": "actor.url", "description": _("URL to the new follower's profile")},
|
||||
# ],
|
||||
# "category": "notifications", # For grouping in UI
|
||||
# },
|
||||
# # Add more editable templates for Blueski here
|
||||
# ]
|
||||
# return templates
|
||||
return [] # Placeholder - no editable templates defined yet for Blueski
|
||||
|
||||
def _get_template_content(self, template_id: str) -> str:
|
||||
"""
|
||||
Retrieves the current content of a specific template, either user-customized or default.
|
||||
"""
|
||||
# config_key = self.config_prefix + template_id
|
||||
# default_value = self._get_default_template_content(template_id)
|
||||
# return approve.config.config.get_value(config_key, default_value) # Example config access
|
||||
return self._get_default_template_content(template_id) # Placeholder
|
||||
|
||||
def _get_default_template_content(self, template_id: str) -> str:
|
||||
"""
|
||||
Returns the default content for a given template ID.
|
||||
"""
|
||||
# This could be hardcoded or loaded from a defaults file.
|
||||
# if template_id == "new_follower_notification":
|
||||
# return "{{ actor.displayName }} (@{{ actor.handle }}) is now following you on Blueski!"
|
||||
# # ... other default templates
|
||||
return "" # Placeholder
|
||||
|
||||
async def save_template_content(self, template_id: str, content: str) -> bool:
|
||||
"""
|
||||
Saves the user-customized content for a specific template.
|
||||
"""
|
||||
# config_key = self.config_prefix + template_id
|
||||
# try:
|
||||
# await approve.config.config.set_value(config_key, content) # Example config access
|
||||
# logger.info(f"Blueski template '{template_id}' saved for user {self.user_id}.")
|
||||
# return True
|
||||
# except Exception as e:
|
||||
# logger.error(f"Error saving Blueski template '{template_id}' for user {self.user_id}: {e}")
|
||||
# return False
|
||||
return False # Placeholder
|
||||
|
||||
def get_template_preview(self, template_id: str, custom_content: str | None = None) -> str:
|
||||
"""
|
||||
Generates a preview of a template using sample data.
|
||||
If custom_content is provided, it's used instead of the saved template.
|
||||
"""
|
||||
# content_to_render = custom_content if custom_content is not None else self._get_template_content(template_id)
|
||||
# sample_data = self._get_sample_data_for_template(template_id)
|
||||
|
||||
# try:
|
||||
# # Use a templating engine (like Jinja2) to render the preview
|
||||
# # from jinja2 import Template
|
||||
# # template = Template(content_to_render)
|
||||
# # preview = template.render(**sample_data)
|
||||
# # return preview
|
||||
# return f"Preview for '{template_id}': {content_to_render}" # Basic placeholder
|
||||
# except Exception as e:
|
||||
# logger.error(f"Error generating preview for Blueski template '{template_id}': {e}")
|
||||
# return _("Error generating preview.")
|
||||
return _("Template previews not yet implemented for Blueski.") # Placeholder
|
||||
|
||||
def _get_sample_data_for_template(self, template_id: str) -> dict[str, Any]:
|
||||
"""
|
||||
Returns sample data appropriate for previewing a specific template.
|
||||
"""
|
||||
# if template_id == "new_follower_notification":
|
||||
# return {
|
||||
# "actor": {
|
||||
# "displayName": "Test User",
|
||||
# "handle": "testuser.bsky.social",
|
||||
# "url": "https://bsky.app/profile/testuser.bsky.social"
|
||||
# }
|
||||
# }
|
||||
# # ... other sample data
|
||||
return {} # Placeholder
|
||||
|
||||
# Functions to be called by the main controller/handler for template editor actions.
|
||||
|
||||
async def get_editor_config(session: BlueskiSession) -> dict[str, Any]:
|
||||
"""
|
||||
Get the configuration needed to display the template editor for Blueski.
|
||||
"""
|
||||
editor = BlueskiTemplateEditor(session)
|
||||
return {
|
||||
"editable_templates": editor.get_editable_templates(),
|
||||
"help_text": _("Customize Blueski message formats. Use variables shown for each template."),
|
||||
}
|
||||
|
||||
async def save_template(session: BlueskiSession, template_id: str, content: str) -> bool:
|
||||
"""
|
||||
Save a modified template for Blueski.
|
||||
"""
|
||||
editor = BlueskiTemplateEditor(session)
|
||||
return await editor.save_template_content(template_id, content)
|
||||
|
||||
async def get_template_preview_html(session: BlueskiSession, template_id: str, content: str) -> str:
|
||||
"""
|
||||
Get an HTML preview for a template with given content.
|
||||
"""
|
||||
editor = BlueskiTemplateEditor(session)
|
||||
return editor.get_template_preview(template_id, custom_content=content)
|
||||
|
||||
|
||||
logger.info("Blueski template editor module loaded (placeholders).")
|
||||
75
src/controller/blueski/userActions.py
Normal file
75
src/controller/blueski/userActions.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
fromapprove.translation import translate as _
|
||||
# fromapprove.controller.mastodon import userActions as mastodon_user_actions # If adapting
|
||||
|
||||
if TYPE_CHECKING:
|
||||
fromapprove.sessions.blueski.session import Session as BlueskiSession # Adjusted
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# This file defines user-specific actions that can be performed on Blueski entities,
|
||||
# typically represented as buttons or links in the UI, often on user profiles or posts.
|
||||
|
||||
# For Blueski, actions might include:
|
||||
# - Viewing a user's profile on Bluesky/Blueski instance.
|
||||
# - Following/Unfollowing a user.
|
||||
# - Muting/Blocking a user.
|
||||
# - Reporting a user.
|
||||
# - Fetching a user's latest posts.
|
||||
|
||||
# These actions are often presented in a context menu or as direct buttons.
|
||||
# The `get_user_actions` method in the BlueskiSession class would define these.
|
||||
# This file would contain the implementation or further handling logic if needed,
|
||||
# or if actions are too complex for simple lambda/method calls in the session class.
|
||||
|
||||
# Example structure for defining an action:
|
||||
# (This might be more detailed if actions require forms or multi-step processes)
|
||||
|
||||
# def view_profile_action(session: BlueskiSession, user_id: str) -> dict[str, Any]:
|
||||
# """
|
||||
# Generates data for a "View Profile on Blueski" action.
|
||||
# user_id here would be the Blueski DID or handle.
|
||||
# """
|
||||
# # profile_url = f"https://bsky.app/profile/{user_id}" # Example, construct from handle or DID
|
||||
# # This might involve resolving DID to handle or vice-versa if only one is known.
|
||||
# # handle = await session.util.get_username_from_user_id(user_id) or user_id
|
||||
# # profile_url = f"https://bsky.app/profile/{handle}"
|
||||
|
||||
# return {
|
||||
# "id": "blueski_view_profile",
|
||||
# "label": _("View Profile on Bluesky"),
|
||||
# "icon": "external-link-alt", # FontAwesome icon name
|
||||
# "action_type": "link", # "link", "modal", "api_call"
|
||||
# "url": profile_url, # For "link" type
|
||||
# # "api_endpoint": "/api/blueski/user_action", # For "api_call"
|
||||
# # "payload": {"action": "view_profile", "target_user_id": user_id},
|
||||
# "confirmation_required": False,
|
||||
# }
|
||||
|
||||
|
||||
# async def follow_user_action_handler(session: BlueskiSession, target_user_id: str) -> dict[str, Any]:
|
||||
# """
|
||||
# Handles the 'follow_user' action for Blueski.
|
||||
# target_user_id should be the DID of the user to follow.
|
||||
# """
|
||||
# # success = await session.util.follow_user(target_user_id)
|
||||
# # if success:
|
||||
# # return {"status": "success", "message": _("User {target_user_id} followed.").format(target_user_id=target_user_id)}
|
||||
# # else:
|
||||
# # return {"status": "error", "message": _("Failed to follow user {target_user_id}.").format(target_user_id=target_user_id)}
|
||||
# return {"status": "pending", "message": "Follow action not implemented yet."}
|
||||
|
||||
|
||||
# The list of available actions is typically defined in the Session class,
|
||||
# e.g., BlueskiSession.get_user_actions(). That method would return a list
|
||||
# of dictionaries, and this file might provide handlers for more complex actions
|
||||
# if they aren't simple API calls defined directly in the session's util.
|
||||
|
||||
# For now, this file can be a placeholder if most actions are simple enough
|
||||
# to be handled directly by the session.util methods or basic handler routes.
|
||||
|
||||
logger.info("Blueski userActions module loaded (placeholders).")
|
||||
225
src/controller/blueski/userList.py
Normal file
225
src/controller/blueski/userList.py
Normal file
@@ -0,0 +1,225 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, AsyncGenerator
|
||||
|
||||
fromapprove.translation import translate as _
|
||||
# fromapprove.controller.mastodon import userList as mastodon_user_list # If adapting
|
||||
|
||||
if TYPE_CHECKING:
|
||||
fromapprove.sessions.blueski.session import Session as BlueskiSession # Adjusted
|
||||
# Define a type for what a user entry in a list might look like for Blueski
|
||||
BlueskiUserListItem = dict[str, Any] # e.g. {"did": "...", "handle": "...", "displayName": "..."}
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# This file is responsible for fetching and managing lists of users from Blueski.
|
||||
# Examples include:
|
||||
# - Followers of a user
|
||||
# - Users a user is following
|
||||
# - Users who liked or reposted a post
|
||||
# - Users in a specific list or feed (if Blueski supports user lists like Twitter/Mastodon)
|
||||
# - Search results for users
|
||||
|
||||
# The structure will likely involve:
|
||||
# - A base class or functions for paginating through user lists from the Blueski API.
|
||||
# - Specific functions for each type of user list.
|
||||
# - Formatting Blueski user data into a consistent structure for UI display.
|
||||
|
||||
async def fetch_followers(
|
||||
session: BlueskiSession,
|
||||
user_id: str, # DID of the user whose followers to fetch
|
||||
limit: int = 20,
|
||||
cursor: str | None = None
|
||||
) -> AsyncGenerator[BlueskiUserListItem, None]:
|
||||
"""
|
||||
Asynchronously fetches a list of followers for a given Blueski user.
|
||||
user_id is the DID of the target user.
|
||||
Yields user data dictionaries.
|
||||
"""
|
||||
# client = await session.util._get_client() # Get authenticated client
|
||||
# if not client:
|
||||
# logger.warning(f"Blueski client not available for fetching followers of {user_id}.")
|
||||
# return
|
||||
|
||||
# current_cursor = cursor
|
||||
# try:
|
||||
# while True:
|
||||
# # response = await client.app.bsky.graph.get_followers(
|
||||
# # models.AppBskyGraphGetFollowers.Params(
|
||||
# # actor=user_id,
|
||||
# # limit=min(limit, 100), # ATProto API might have its own max limit per request (e.g. 100)
|
||||
# # cursor=current_cursor
|
||||
# # )
|
||||
# # )
|
||||
# # if not response or not response.followers:
|
||||
# # break
|
||||
|
||||
# # for user_profile_view in response.followers:
|
||||
# # yield session.util._format_profile_data(user_profile_view) # Use a utility to standardize format
|
||||
|
||||
# # current_cursor = response.cursor
|
||||
# # if not current_cursor or len(response.followers) < limit : # Or however the API indicates end of list
|
||||
# # break
|
||||
|
||||
# # This is a placeholder loop for demonstration
|
||||
# if current_cursor == "simulated_end_cursor": break # Stop after one simulated page
|
||||
# for i in range(limit):
|
||||
# if current_cursor and int(current_cursor) + i >= 25: # Simulate total 25 followers
|
||||
# current_cursor = "simulated_end_cursor"
|
||||
# break
|
||||
# yield {
|
||||
# "did": f"did:plc:follower{i + (int(current_cursor) if current_cursor else 0)}",
|
||||
# "handle": f"follower{i + (int(current_cursor) if current_cursor else 0)}.bsky.social",
|
||||
# "displayName": f"Follower {i + (int(current_cursor) if current_cursor else 0)}",
|
||||
# "avatar": None # Placeholder
|
||||
# }
|
||||
# if not current_cursor: current_cursor = str(limit) # Simulate next cursor
|
||||
# elif current_cursor != "simulated_end_cursor": current_cursor = str(int(current_cursor) + limit)
|
||||
|
||||
|
||||
"""
|
||||
if not session.is_ready():
|
||||
logger.warning(f"Cannot fetch followers for {user_id}: Blueski session not ready.")
|
||||
# yield {} # Stop iteration if not ready
|
||||
return
|
||||
|
||||
try:
|
||||
followers_data = await session.util.get_followers(user_did=user_id, limit=limit, cursor=cursor)
|
||||
if followers_data:
|
||||
users, _ = followers_data # We'll return the cursor separately via the calling HTTP handler
|
||||
for user_profile_view in users:
|
||||
yield session.util._format_profile_data(user_profile_view)
|
||||
else:
|
||||
logger.info(f"No followers data returned for user {user_id}.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in fetch_followers for Blueski user {user_id}: {e}", exc_info=True)
|
||||
# Depending on desired error handling, could raise or yield an error marker
|
||||
|
||||
|
||||
async def fetch_following(
|
||||
session: BlueskiSession,
|
||||
user_id: str, # DID of the user whose followed accounts to fetch
|
||||
limit: int = 20,
|
||||
cursor: str | None = None
|
||||
) -> AsyncGenerator[BlueskiUserListItem, None]:
|
||||
"""
|
||||
Asynchronously fetches a list of users followed by a given Blueski user.
|
||||
Yields user data dictionaries.
|
||||
"""
|
||||
if not session.is_ready():
|
||||
logger.warning(f"Cannot fetch following for {user_id}: Blueski session not ready.")
|
||||
return
|
||||
|
||||
try:
|
||||
following_data = await session.util.get_following(user_did=user_id, limit=limit, cursor=cursor)
|
||||
if following_data:
|
||||
users, _ = following_data
|
||||
for user_profile_view in users:
|
||||
yield session.util._format_profile_data(user_profile_view)
|
||||
else:
|
||||
logger.info(f"No following data returned for user {user_id}.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in fetch_following for Blueski user {user_id}: {e}", exc_info=True)
|
||||
|
||||
|
||||
async def search_users(
|
||||
session: BlueskiSession,
|
||||
query: str,
|
||||
limit: int = 20,
|
||||
cursor: str | None = None
|
||||
) -> AsyncGenerator[BlueskiUserListItem, None]:
|
||||
"""
|
||||
Searches for users on Blueski based on a query string.
|
||||
Yields user data dictionaries.
|
||||
"""
|
||||
if not session.is_ready():
|
||||
logger.warning(f"Cannot search users for '{query}': Blueski session not ready.")
|
||||
return
|
||||
|
||||
try:
|
||||
search_data = await session.util.search_users(term=query, limit=limit, cursor=cursor)
|
||||
if search_data:
|
||||
users, _ = search_data
|
||||
for user_profile_view in users:
|
||||
yield session.util._format_profile_data(user_profile_view)
|
||||
else:
|
||||
logger.info(f"No users found for search term '{query}'.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in search_users for Blueski query '{query}': {e}", exc_info=True)
|
||||
|
||||
# This function is designed to be called by an API endpoint that returns JSON
|
||||
async def get_user_list_paginated(
|
||||
session: BlueskiSession,
|
||||
list_type: str, # "followers", "following", "search"
|
||||
identifier: str, # User DID for followers/following, or search query for search
|
||||
limit: int = 20,
|
||||
cursor: str | None = None
|
||||
) -> tuple[list[BlueskiUserListItem], str | None]:
|
||||
"""
|
||||
Fetches a paginated list of users (followers, following, or search results)
|
||||
and returns the list and the next cursor.
|
||||
"""
|
||||
users_list: list[BlueskiUserListItem] = []
|
||||
next_cursor: str | None = None
|
||||
|
||||
if not session.is_ready():
|
||||
logger.warning(f"Cannot fetch user list '{list_type}': Blueski session not ready.")
|
||||
return [], None
|
||||
|
||||
try:
|
||||
if list_type == "followers":
|
||||
data = await session.util.get_followers(user_did=identifier, limit=limit, cursor=cursor)
|
||||
if data: users_list = [session.util._format_profile_data(u) for u in data[0]]; next_cursor = data[1]
|
||||
elif list_type == "following":
|
||||
data = await session.util.get_following(user_did=identifier, limit=limit, cursor=cursor)
|
||||
if data: users_list = [session.util._format_profile_data(u) for u in data[0]]; next_cursor = data[1]
|
||||
elif list_type == "search_users":
|
||||
data = await session.util.search_users(term=identifier, limit=limit, cursor=cursor)
|
||||
if data: users_list = [session.util._format_profile_data(u) for u in data[0]]; next_cursor = data[1]
|
||||
else:
|
||||
logger.error(f"Unknown list_type: {list_type}")
|
||||
return [], None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching paginated user list '{list_type}' for '{identifier}': {e}", exc_info=True)
|
||||
# Optionally re-raise or return empty with no cursor to indicate error
|
||||
return [], None
|
||||
|
||||
return users_list, next_cursor
|
||||
|
||||
|
||||
async def get_user_profile_details(session: BlueskiSession, user_ident: str) -> BlueskiUserListItem | None:
|
||||
"""
|
||||
Fetches detailed profile information for a user by DID or handle.
|
||||
Returns a dictionary of formatted profile data, or None if not found/error.
|
||||
"""
|
||||
if not session.is_ready():
|
||||
logger.warning(f"Cannot get profile for {user_ident}: Blueski session not ready.")
|
||||
return None
|
||||
|
||||
try:
|
||||
profile_view_detailed = await session.util.get_user_profile(user_ident=user_ident)
|
||||
if profile_view_detailed:
|
||||
return session.util._format_profile_data(profile_view_detailed)
|
||||
else:
|
||||
logger.info(f"No profile data found for user {user_ident}.")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error in get_user_profile_details for {user_ident}: {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
# Other list types could include:
|
||||
# - fetch_likers(session, post_uri, limit, cursor) # Needs app.bsky.feed.getLikes
|
||||
# - fetch_reposters(session, post_uri, limit, cursor)
|
||||
# - fetch_muted_users(session, limit, cursor)
|
||||
# - fetch_blocked_users(session, limit, cursor)
|
||||
|
||||
# The UI part of Approve that displays user lists would call these functions.
|
||||
# Each function needs to handle pagination as provided by the ATProto API (usually cursor-based).
|
||||
|
||||
logger.info("Blueski userList module loaded (placeholders).")
|
||||
Reference in New Issue
Block a user