This commit is contained in:
Jesús Pavón Abián
2026-01-10 19:46:53 +01:00
55 changed files with 1504 additions and 407 deletions

View File

@@ -24,13 +24,13 @@ class Handler:
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)
# Home timeline only for now
# Discover/home timeline
from pubsub import pub
pub.sendMessage(
"createBuffer",
buffer_type="home_timeline",
session_type="atprotosocial",
buffer_title=_("Home"),
session_type="blueski",
buffer_title=_("Discover"),
parent_tab=root_position,
start=True,
kwargs=dict(parent=controller.view.nb, name="home_timeline", session=session)
@@ -39,8 +39,8 @@ class Handler:
pub.sendMessage(
"createBuffer",
buffer_type="following_timeline",
session_type="atprotosocial",
buffer_title=_("Following"),
session_type="blueski",
buffer_title=_("Following (Chronological)"),
parent_tab=root_position,
start=False,
kwargs=dict(parent=controller.view.nb, name="following_timeline", session=session)
@@ -71,7 +71,7 @@ class Handler:
current_mode = None
ask_default = True if current_mode in (None, "ask") else False
from wxUI.dialogs.atprotosocial.configuration import AccountSettingsDialog
from wxUI.dialogs.blueski.configuration import AccountSettingsDialog
dlg = AccountSettingsDialog(controller.view, ask_before_boost=ask_default)
resp = dlg.ShowModal()
if resp == wx.ID_OK:

View File

@@ -8,34 +8,34 @@ from typing import Any
logger = logging.getLogger(__name__)
# This file would typically contain functions to generate complex message bodies or
# interactive components for ATProtoSocial, similar to how it might be done for Mastodon.
# Since ATProtoSocial's interactive features (beyond basic posts) are still evolving
# 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 ATProtoSocial develops a standard for "cards" or interactive messages,
# 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 ATProtoSocial session.
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 ATProtoSocial account")) if user_profile else _("your ATProtoSocial account")
# 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 *_: {})("atprotosocial").get("handle")
or getattr(getattr(session, "settings", {}), "get", lambda *_: {})("blueski").get("handle")
or _("your Bluesky account"))
return {
"text": _("Welcome to Approve for ATProtoSocial! Your account {handle} is connected.").format(handle=handle),
# "blocks": [ # If ATProtoSocial supports a block kit like Slack or Discord
"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 ATProtoSocial's equivalent
# "text": _("Welcome to Approve for ATProtoSocial! Your account *{handle}* is connected.").format(handle=handle)
# "type": "mrkdwn", # Or Blueski's equivalent
# "text": _("Welcome to Approve for Blueski! Your account *{handle}* is connected.").format(handle=handle)
# }
# },
# {
@@ -44,7 +44,7 @@ def format_welcome_message(session: Any) -> dict[str, Any]:
# {
# "type": "button",
# "text": {"type": "plain_text", "text": _("Post your first Skeet")},
# "action_id": "atprotosocial_compose_new_post" # Example action ID
# "action_id": "blueski_compose_new_post" # Example action ID
# }
# ]
# }
@@ -65,16 +65,16 @@ def format_error_message(error_description: str, details: str | None = None) ->
# ]
return message
# More functions could be added here as ATProtoSocial's capabilities become clearer
# 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 ATProtoSocial features.
# - 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: ATProtoSocialSession, post_uri: str, post_content: dict) -> dict[str, Any]:
# def build_post_summary_message(session: BlueskiSession, post_uri: str, post_content: dict) -> dict[str, Any]:
# """
# Builds a summary message for an ATProtoSocial post.
# 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
@@ -88,4 +88,4 @@ def format_error_message(error_description: str, details: str | None = None) ->
# # Potentially with "blocks" for richer formatting if the platform supports it
# }
logger.info("ATProtoSocial messages module loaded (placeholders).")
logger.info("Blueski messages module loaded (placeholders).")

View File

@@ -8,29 +8,29 @@ fromapprove.translation import translate as _
if TYPE_CHECKING:
fromapprove.config import ConfigSectionProxy
fromapprove.sessions.atprotosocial.session import Session as ATProtoSocialSession # Adjusted
fromapprove.sessions.blueski.session import Session as BlueskiSession # Adjusted
logger = logging.getLogger(__name__)
# This file is for defining forms and handling for ATProtoSocial-specific settings
# 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 ATProtoSocial, initial settings might be simple (handle, app password),
# For Blueski, initial settings might be simple (handle, app password),
# but this structure allows for expansion.
class ATProtoSocialSettingsForm(Form):
class BlueskiSettingsForm(Form):
"""
A settings form for ATProtoSocial sessions.
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 ATProtoSocialSession.get_settings_inputs defines
# and what ATProtoSocialSession.get_configurable_values expects for its config.
# 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 ATProtoSocial PDS instance (e.g., https://bsky.social)."),
# 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(
@@ -43,19 +43,19 @@ class ATProtoSocialSettingsForm(Form):
description=_("Your Bluesky App Password. Generate this in your Bluesky account settings."),
validators=[], # e.g., DataRequired()
)
# Add more fields as needed for ATProtoSocial configuration.
# Add more fields as needed for Blueski configuration.
# For example, if there were specific notification settings, content filters, etc.
submit = SubmitField(_("Save ATProtoSocial Settings"))
submit = SubmitField(_("Save Blueski Settings"))
async def get_settings_form(
user_id: str,
session: ATProtoSocialSession | None = None,
config: ConfigSectionProxy | None = None, # User-specific config for ATProtoSocial
) -> ATProtoSocialSettingsForm:
session: BlueskiSession | None = None,
config: ConfigSectionProxy | None = None, # User-specific config for Blueski
) -> BlueskiSettingsForm:
"""
Creates and pre-populates the ATProtoSocial settings form.
Creates and pre-populates the Blueski settings form.
"""
form_data = {}
if session: # If a session exists, use its current config
@@ -68,31 +68,31 @@ async def get_settings_form(
form_data["handle"] = config.handle.get("")
form_data["app_password"] = ""
form = ATProtoSocialSettingsForm(formdata=None, **form_data) # formdata=None for initial display
form = BlueskiSettingsForm(formdata=None, **form_data) # formdata=None for initial display
return form
async def process_settings_form(
form: ATProtoSocialSettingsForm,
form: BlueskiSettingsForm,
user_id: str,
session: ATProtoSocialSession | None = None, # Pass if update should affect live session
config: ConfigSectionProxy | None = None, # User-specific config for ATProtoSocial
session: BlueskiSession | None = None, # Pass if update should affect live session
config: ConfigSectionProxy | None = None, # User-specific config for Blueski
) -> bool:
"""
Processes the submitted ATProtoSocial settings form and updates configuration.
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"ATProtoSocial settings form validation failed for user {user_id}: {form.errors}")
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.atprotosocial[user_id] # Example path
# 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 ATProtoSocial settings for user {user_id}: no config proxy available.")
logger.error(f"Cannot process Blueski settings for user {user_id}: no config proxy available.")
return False
try:
@@ -101,11 +101,11 @@ async def process_settings_form(
await config.handle.set(form.handle.data)
await config.app_password.set(form.app_password.data) # Ensure this is stored securely
logger.info(f"ATProtoSocial settings updated for user {user_id}.")
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 ATProtoSocial session re-initialization for user {user_id} due to settings change.")
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
@@ -118,11 +118,11 @@ async def process_settings_form(
return True
except Exception as e:
logger.error(f"Error saving ATProtoSocial settings for user {user_id}: {e}", exc_info=True)
logger.error(f"Error saving Blueski settings for user {user_id}: {e}", exc_info=True)
return False
# Any additional ATProtoSocial-specific settings views or handlers would go here.
# For instance, if ATProtoSocial had features like "Relays" or "Feed Generators"
# 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("ATProtoSocial settings module loaded (placeholders).")
logger.info("Blueski settings module loaded (placeholders).")

View File

@@ -7,29 +7,29 @@ from typing import TYPE_CHECKING, Any
fromapprove.translation import translate as _
if TYPE_CHECKING:
fromapprove.sessions.atprotosocial.session import Session as ATProtoSocialSession # Adjusted
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 ATProtoSocial.
# 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 ATProtoSocial are displayed in Approve.
# from Blueski are displayed in Approve.
# For ATProtoSocial, this might be less relevant initially if its content structure
# 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 ATProtoSocialTemplateEditor:
def __init__(self, session: ATProtoSocialSession) -> None:
class BlueskiTemplateEditor:
def __init__(self, session: BlueskiSession) -> None:
self.session = session
# self.user_id = session.user_id
# self.config_prefix = f"sessions.atprotosocial.{self.user_id}.templates." # Example config path
# 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 ATProtoSocial.
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
@@ -40,8 +40,8 @@ class ATProtoSocialTemplateEditor:
# {
# "id": "new_follower_notification", # Unique ID for this template
# "name": _("New Follower Notification Format"),
# "description": _("Customize how new follower notifications from ATProtoSocial are displayed."),
# "default_template": "{{ actor.displayName }} (@{{ actor.handle }}) is now following you on ATProtoSocial!",
# "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")},
@@ -50,10 +50,10 @@ class ATProtoSocialTemplateEditor:
# ],
# "category": "notifications", # For grouping in UI
# },
# # Add more editable templates for ATProtoSocial here
# # Add more editable templates for Blueski here
# ]
# return templates
return [] # Placeholder - no editable templates defined yet for ATProtoSocial
return [] # Placeholder - no editable templates defined yet for Blueski
def _get_template_content(self, template_id: str) -> str:
"""
@@ -70,7 +70,7 @@ class ATProtoSocialTemplateEditor:
"""
# 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 ATProtoSocial!"
# return "{{ actor.displayName }} (@{{ actor.handle }}) is now following you on Blueski!"
# # ... other default templates
return "" # Placeholder
@@ -81,10 +81,10 @@ class ATProtoSocialTemplateEditor:
# config_key = self.config_prefix + template_id
# try:
# await approve.config.config.set_value(config_key, content) # Example config access
# logger.info(f"ATProtoSocial template '{template_id}' saved for user {self.user_id}.")
# logger.info(f"Blueski template '{template_id}' saved for user {self.user_id}.")
# return True
# except Exception as e:
# logger.error(f"Error saving ATProtoSocial template '{template_id}' for user {self.user_id}: {e}")
# logger.error(f"Error saving Blueski template '{template_id}' for user {self.user_id}: {e}")
# return False
return False # Placeholder
@@ -104,9 +104,9 @@ class ATProtoSocialTemplateEditor:
# # return preview
# return f"Preview for '{template_id}': {content_to_render}" # Basic placeholder
# except Exception as e:
# logger.error(f"Error generating preview for ATProtoSocial template '{template_id}': {e}")
# logger.error(f"Error generating preview for Blueski template '{template_id}': {e}")
# return _("Error generating preview.")
return _("Template previews not yet implemented for ATProtoSocial.") # Placeholder
return _("Template previews not yet implemented for Blueski.") # Placeholder
def _get_sample_data_for_template(self, template_id: str) -> dict[str, Any]:
"""
@@ -125,29 +125,29 @@ class ATProtoSocialTemplateEditor:
# Functions to be called by the main controller/handler for template editor actions.
async def get_editor_config(session: ATProtoSocialSession) -> dict[str, Any]:
async def get_editor_config(session: BlueskiSession) -> dict[str, Any]:
"""
Get the configuration needed to display the template editor for ATProtoSocial.
Get the configuration needed to display the template editor for Blueski.
"""
editor = ATProtoSocialTemplateEditor(session)
editor = BlueskiTemplateEditor(session)
return {
"editable_templates": editor.get_editable_templates(),
"help_text": _("Customize ATProtoSocial message formats. Use variables shown for each template."),
"help_text": _("Customize Blueski message formats. Use variables shown for each template."),
}
async def save_template(session: ATProtoSocialSession, template_id: str, content: str) -> bool:
async def save_template(session: BlueskiSession, template_id: str, content: str) -> bool:
"""
Save a modified template for ATProtoSocial.
Save a modified template for Blueski.
"""
editor = ATProtoSocialTemplateEditor(session)
editor = BlueskiTemplateEditor(session)
return await editor.save_template_content(template_id, content)
async def get_template_preview_html(session: ATProtoSocialSession, template_id: str, content: str) -> str:
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 = ATProtoSocialTemplateEditor(session)
editor = BlueskiTemplateEditor(session)
return editor.get_template_preview(template_id, custom_content=content)
logger.info("ATProtoSocial template editor module loaded (placeholders).")
logger.info("Blueski template editor module loaded (placeholders).")

View File

@@ -7,32 +7,32 @@ fromapprove.translation import translate as _
# fromapprove.controller.mastodon import userActions as mastodon_user_actions # If adapting
if TYPE_CHECKING:
fromapprove.sessions.atprotosocial.session import Session as ATProtoSocialSession # Adjusted
fromapprove.sessions.blueski.session import Session as BlueskiSession # Adjusted
logger = logging.getLogger(__name__)
# This file defines user-specific actions that can be performed on ATProtoSocial entities,
# 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 ATProtoSocial, actions might include:
# - Viewing a user's profile on Bluesky/ATProtoSocial instance.
# 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 ATProtoSocialSession class would define these.
# 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: ATProtoSocialSession, user_id: str) -> dict[str, Any]:
# def view_profile_action(session: BlueskiSession, user_id: str) -> dict[str, Any]:
# """
# Generates data for a "View Profile on ATProtoSocial" action.
# user_id here would be the ATProtoSocial DID or handle.
# 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.
@@ -40,20 +40,20 @@ logger = logging.getLogger(__name__)
# # profile_url = f"https://bsky.app/profile/{handle}"
# return {
# "id": "atprotosocial_view_profile",
# "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/atprotosocial/user_action", # For "api_call"
# # "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: ATProtoSocialSession, target_user_id: str) -> dict[str, Any]:
# async def follow_user_action_handler(session: BlueskiSession, target_user_id: str) -> dict[str, Any]:
# """
# Handles the 'follow_user' action for ATProtoSocial.
# 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)
@@ -65,11 +65,11 @@ logger = logging.getLogger(__name__)
# The list of available actions is typically defined in the Session class,
# e.g., ATProtoSocialSession.get_user_actions(). That method would return a list
# 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("ATProtoSocial userActions module loaded (placeholders).")
logger.info("Blueski userActions module loaded (placeholders).")

View File

@@ -7,39 +7,39 @@ fromapprove.translation import translate as _
# fromapprove.controller.mastodon import userList as mastodon_user_list # If adapting
if TYPE_CHECKING:
fromapprove.sessions.atprotosocial.session import Session as ATProtoSocialSession # Adjusted
# Define a type for what a user entry in a list might look like for ATProtoSocial
ATProtoSocialUserListItem = dict[str, Any] # e.g. {"did": "...", "handle": "...", "displayName": "..."}
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 ATProtoSocial.
# 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 ATProtoSocial supports user lists like Twitter/Mastodon)
# - 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 ATProtoSocial API.
# - A base class or functions for paginating through user lists from the Blueski API.
# - Specific functions for each type of user list.
# - Formatting ATProtoSocial user data into a consistent structure for UI display.
# - Formatting Blueski user data into a consistent structure for UI display.
async def fetch_followers(
session: ATProtoSocialSession,
session: BlueskiSession,
user_id: str, # DID of the user whose followers to fetch
limit: int = 20,
cursor: str | None = None
) -> AsyncGenerator[ATProtoSocialUserListItem, None]:
) -> AsyncGenerator[BlueskiUserListItem, None]:
"""
Asynchronously fetches a list of followers for a given ATProtoSocial user.
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"ATProtoSocial client not available for fetching followers of {user_id}.")
# logger.warning(f"Blueski client not available for fetching followers of {user_id}.")
# return
# current_cursor = cursor
@@ -80,7 +80,7 @@ async def fetch_followers(
"""
if not session.is_ready():
logger.warning(f"Cannot fetch followers for {user_id}: ATProtoSocial session not ready.")
logger.warning(f"Cannot fetch followers for {user_id}: Blueski session not ready.")
# yield {} # Stop iteration if not ready
return
@@ -94,22 +94,22 @@ async def fetch_followers(
logger.info(f"No followers data returned for user {user_id}.")
except Exception as e:
logger.error(f"Error in fetch_followers for ATProtoSocial user {user_id}: {e}", exc_info=True)
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: ATProtoSocialSession,
session: BlueskiSession,
user_id: str, # DID of the user whose followed accounts to fetch
limit: int = 20,
cursor: str | None = None
) -> AsyncGenerator[ATProtoSocialUserListItem, None]:
) -> AsyncGenerator[BlueskiUserListItem, None]:
"""
Asynchronously fetches a list of users followed by a given ATProtoSocial user.
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}: ATProtoSocial session not ready.")
logger.warning(f"Cannot fetch following for {user_id}: Blueski session not ready.")
return
try:
@@ -122,21 +122,21 @@ async def fetch_following(
logger.info(f"No following data returned for user {user_id}.")
except Exception as e:
logger.error(f"Error in fetch_following for ATProtoSocial user {user_id}: {e}", exc_info=True)
logger.error(f"Error in fetch_following for Blueski user {user_id}: {e}", exc_info=True)
async def search_users(
session: ATProtoSocialSession,
session: BlueskiSession,
query: str,
limit: int = 20,
cursor: str | None = None
) -> AsyncGenerator[ATProtoSocialUserListItem, None]:
) -> AsyncGenerator[BlueskiUserListItem, None]:
"""
Searches for users on ATProtoSocial based on a query string.
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}': ATProtoSocial session not ready.")
logger.warning(f"Cannot search users for '{query}': Blueski session not ready.")
return
try:
@@ -149,25 +149,25 @@ async def search_users(
logger.info(f"No users found for search term '{query}'.")
except Exception as e:
logger.error(f"Error in search_users for ATProtoSocial query '{query}': {e}", exc_info=True)
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: ATProtoSocialSession,
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[ATProtoSocialUserListItem], str | 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[ATProtoSocialUserListItem] = []
users_list: list[BlueskiUserListItem] = []
next_cursor: str | None = None
if not session.is_ready():
logger.warning(f"Cannot fetch user list '{list_type}': ATProtoSocial session not ready.")
logger.warning(f"Cannot fetch user list '{list_type}': Blueski session not ready.")
return [], None
try:
@@ -192,13 +192,13 @@ async def get_user_list_paginated(
return users_list, next_cursor
async def get_user_profile_details(session: ATProtoSocialSession, user_ident: str) -> ATProtoSocialUserListItem | None:
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}: ATProtoSocial session not ready.")
logger.warning(f"Cannot get profile for {user_ident}: Blueski session not ready.")
return None
try:
@@ -222,4 +222,4 @@ async def get_user_profile_details(session: ATProtoSocialSession, user_ident: st
# 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("ATProtoSocial userList module loaded (placeholders).")
logger.info("Blueski userList module loaded (placeholders).")

View File

@@ -280,6 +280,12 @@ class BaseBuffer(base.Buffer):
return
menu = menus.base()
widgetUtils.connect_event(menu, widgetUtils.MENU, self.reply, menuitem=menu.reply)
# Enable/disable edit based on whether the post belongs to the user
item = self.get_item()
if item and item.account.id == self.session.db["user_id"] and item.reblog == None:
widgetUtils.connect_event(menu, widgetUtils.MENU, self.edit_status, menuitem=menu.edit)
else:
menu.edit.Enable(False)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.user_actions, menuitem=menu.userActions)
if self.can_share() == True:
widgetUtils.connect_event(menu, widgetUtils.MENU, self.share_item, menuitem=menu.boost)
@@ -501,6 +507,49 @@ class BaseBuffer(base.Buffer):
log.exception("")
self.session.db[self.name] = items
def edit_status(self, event=None, item=None, *args, **kwargs):
if item == None:
item = self.get_item()
# Check if the post belongs to the current user
if item.account.id != self.session.db["user_id"] or item.reblog != None:
output.speak(_("You can only edit your own posts."))
return
# Check if post has a poll with votes - warn user before proceeding
if hasattr(item, 'poll') and item.poll is not None:
votes_count = item.poll.votes_count if hasattr(item.poll, 'votes_count') else 0
if votes_count > 0:
# Show confirmation dialog
warning_title = _("Warning: Poll with votes")
warning_message = _("This post contains a poll with {votes} votes.\n\n"
"According to Mastodon's API, editing this post will reset ALL votes to zero, "
"even if you don't modify the poll itself.\n\n"
"Do you want to continue editing?").format(votes=votes_count)
dialog = wx.MessageDialog(self.buffer, warning_message, warning_title,
wx.YES_NO | wx.NO_DEFAULT | wx.ICON_WARNING)
result = dialog.ShowModal()
dialog.Destroy()
if result != wx.ID_YES:
output.speak(_("Edit cancelled"))
return
# Log item info for debugging
log.debug("Editing status: id={}, has_media_attachments={}, media_count={}".format(
item.id,
hasattr(item, 'media_attachments'),
len(item.media_attachments) if hasattr(item, 'media_attachments') else 0
))
# Create edit dialog with existing post data
title = _("Edit post")
caption = _("Edit your post here")
post = messages.editPost(session=self.session, item=item, title=title, caption=caption)
response = post.message.ShowModal()
if response == wx.ID_OK:
post_data = post.get_data()
# Call edit_post method in session
# Note: visibility and language cannot be changed when editing per Mastodon API
call_threaded(self.session.edit_post, post_id=post.post_id, posts=post_data)
if hasattr(post.message, "destroy"):
post.message.destroy()
def user_details(self):
item = self.get_item()
pass

View File

@@ -161,6 +161,13 @@ class NotificationsBuffer(BaseBuffer):
menu = menus.notification(notification.type)
if self.is_post():
widgetUtils.connect_event(menu, widgetUtils.MENU, self.reply, menuitem=menu.reply)
# Enable/disable edit based on whether the post belongs to the user
if hasattr(menu, 'edit'):
status = self.get_post()
if status and status.account.id == self.session.db["user_id"] and status.reblog == None:
widgetUtils.connect_event(menu, widgetUtils.MENU, self.edit_status, menuitem=menu.edit)
else:
menu.edit.Enable(False)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.user_actions, menuitem=menu.userActions)
if self.can_share() == True:
widgetUtils.connect_event(menu, widgetUtils.MENU, self.share_item, menuitem=menu.boost)

View File

@@ -25,7 +25,7 @@ from mysc import localization
from mysc.thread_utils import call_threaded
from mysc.repeating_timer import RepeatingTimer
from controller.mastodon import handler as MastodonHandler
from controller.atprotosocial import handler as ATProtoSocialHandler # Added import
from controller.blueski import handler as BlueskiHandler # Added import
from . import settings, userAlias
log = logging.getLogger("mainController")
@@ -99,8 +99,8 @@ class Controller(object):
try:
if type == "mastodon":
return MastodonHandler.Handler()
if type == "atprotosocial":
return ATProtoSocialHandler.Handler()
if type == "blueski":
return BlueskiHandler.Handler()
except Exception:
log.exception("Error creating handler for type %s", type)
return None
@@ -207,8 +207,8 @@ class Controller(object):
if handler is None:
if type == "mastodon":
handler = MastodonHandler.Handler()
elif type == "atprotosocial":
handler = ATProtoSocialHandler.Handler()
elif type == "blueski":
handler = BlueskiHandler.Handler()
self.handlers[type] = handler
return handler
@@ -250,9 +250,9 @@ class Controller(object):
for i in sessions.sessions:
log.debug("Working on session %s" % (i,))
if sessions.sessions[i].is_logged == False:
# Try auto-login for ATProtoSocial sessions if credentials exist
# Try auto-login for Blueski sessions if credentials exist
try:
if getattr(sessions.sessions[i], "type", None) == "atprotosocial":
if getattr(sessions.sessions[i], "type", None) == "blueski":
sessions.sessions[i].login()
except Exception:
log.exception("Auto-login attempt failed for session %s", i)
@@ -260,7 +260,7 @@ class Controller(object):
self.create_ignored_session_buffer(sessions.sessions[i])
continue
# Supported session types
valid_session_types = ["mastodon", "atprotosocial"]
valid_session_types = ["mastodon", "blueski"]
if sessions.sessions[i].type in valid_session_types:
try:
handler = self.get_handler(type=sessions.sessions[i].type)
@@ -323,15 +323,15 @@ class Controller(object):
log.debug("Creating buffer of type {0} with parent_tab of {2} arguments {1}".format(buffer_type, kwargs, parent_tab))
if kwargs.get("parent") == None:
kwargs["parent"] = self.view.nb
if not hasattr(buffers, session_type) and session_type != "atprotosocial": # Allow atprotosocial to be handled separately
if not hasattr(buffers, session_type) and session_type != "blueski": # Allow blueski to be handled separately
raise AttributeError("Session type %s does not exist yet." % (session_type))
try:
buffer_panel_class = None
if session_type == "atprotosocial":
from wxUI.buffers.atprotosocial import panels as ATProtoSocialPanels # Import new panels
if session_type == "blueski":
from wxUI.buffers.blueski import panels as BlueskiPanels # Import new panels
if buffer_type == "home_timeline":
buffer_panel_class = ATProtoSocialPanels.ATProtoSocialHomeTimelinePanel
buffer_panel_class = BlueskiPanels.BlueskiHomeTimelinePanel
# kwargs for HomeTimelinePanel: parent, name, session
# 'name' is buffer_title, 'parent' is self.view.nb
# 'session' needs to be fetched based on user_id in kwargs
@@ -343,38 +343,38 @@ class Controller(object):
if "name" not in kwargs: kwargs["name"] = buffer_title
elif buffer_type == "user_timeline":
buffer_panel_class = ATProtoSocialPanels.ATProtoSocialUserTimelinePanel
buffer_panel_class = BlueskiPanels.BlueskiUserTimelinePanel
# kwargs for UserTimelinePanel: parent, name, session, target_user_did, target_user_handle
if "user_id" in kwargs and "session" not in kwargs:
kwargs["session"] = sessions.sessions.get(kwargs["user_id"])
kwargs.pop("user_id", None)
if "name" not in kwargs: kwargs["name"] = buffer_title
# target_user_did and target_user_handle must be in kwargs from atprotosocial.Handler
# target_user_did and target_user_handle must be in kwargs from blueski.Handler
elif buffer_type == "notifications":
buffer_panel_class = ATProtoSocialPanels.ATProtoSocialNotificationPanel
buffer_panel_class = BlueskiPanels.BlueskiNotificationPanel
if "user_id" in kwargs and "session" not in kwargs:
kwargs["session"] = sessions.sessions.get(kwargs["user_id"])
kwargs.pop("user_id", None)
if "name" not in kwargs: kwargs["name"] = buffer_title
# target_user_did and target_user_handle must be in kwargs from atprotosocial.Handler
# target_user_did and target_user_handle must be in kwargs from blueski.Handler
elif buffer_type == "notifications":
buffer_panel_class = ATProtoSocialPanels.ATProtoSocialNotificationPanel
buffer_panel_class = BlueskiPanels.BlueskiNotificationPanel
if "user_id" in kwargs and "session" not in kwargs:
kwargs["session"] = sessions.sessions.get(kwargs["user_id"])
kwargs.pop("user_id", None)
if "name" not in kwargs: kwargs["name"] = buffer_title
elif buffer_type == "user_list_followers" or buffer_type == "user_list_following":
buffer_panel_class = ATProtoSocialPanels.ATProtoSocialUserListPanel
buffer_panel_class = BlueskiPanels.BlueskiUserListPanel
elif buffer_type == "following_timeline":
buffer_panel_class = ATProtoSocialPanels.ATProtoSocialFollowingTimelinePanel
buffer_panel_class = BlueskiPanels.BlueskiFollowingTimelinePanel
# Clean stray keys that this panel doesn't accept
kwargs.pop("user_id", None)
kwargs.pop("list_type", None)
if "name" not in kwargs: kwargs["name"] = buffer_title
else:
log.warning(f"Unsupported ATProtoSocial buffer type: {buffer_type}. Falling back to generic.")
log.warning(f"Unsupported Blueski buffer type: {buffer_type}. Falling back to generic.")
# Fallback to trying to find it in generic buffers or error
available_buffers = getattr(buffers, "base", None) # Or some generic panel module
if available_buffers and hasattr(available_buffers, buffer_type):
@@ -382,7 +382,7 @@ class Controller(object):
elif available_buffers and hasattr(available_buffers, "TimelinePanel"): # Example generic
buffer_panel_class = getattr(available_buffers, "TimelinePanel")
else:
raise AttributeError(f"ATProtoSocial buffer type {buffer_type} not found in atprotosocial.panels or base panels.")
raise AttributeError(f"Blueski buffer type {buffer_type} not found in blueski.panels or base panels.")
else: # Existing logic for other session types
available_buffers = getattr(buffers, session_type)
if not hasattr(available_buffers, buffer_type):
@@ -549,6 +549,15 @@ class Controller(object):
buffer = self.search_buffer(buffer.name, buffer.account)
buffer.destroy_status()
def edit_post(self, *args, **kwargs):
""" Edits a post in the current buffer.
Users can only edit their own posts."""
buffer = self.view.get_current_buffer()
if hasattr(buffer, "account"):
buffer = self.search_buffer(buffer.name, buffer.account)
if hasattr(buffer, "edit_status"):
buffer.edit_status()
def exit(self, *args, **kwargs):
if config.app["app-settings"]["ask_at_exit"] == True:
answer = commonMessageDialogs.exit_dialog(self.view)
@@ -598,7 +607,7 @@ class Controller(object):
session = buffer.session
# Compose for Bluesky (ATProto): dialog with attachments/CW/language
if getattr(session, "type", "") == "atprotosocial":
if getattr(session, "type", "") == "blueski":
# In invisible interface, prefer a quick, minimal compose to avoid complex UI
if self.showing == False:
# Parent=None so it shows even if main window is hidden
@@ -620,7 +629,7 @@ class Controller(object):
else:
dlg.Destroy()
return
from wxUI.dialogs.atprotosocial.postDialogs import Post as ATPostDialog
from wxUI.dialogs.blueski.postDialogs import Post as ATPostDialog
dlg = ATPostDialog()
if dlg.ShowModal() == wx.ID_OK:
text, files, cw_text, langs = dlg.get_payload()
@@ -712,7 +721,7 @@ class Controller(object):
return
session = buffer.session
if getattr(session, "type", "") == "atprotosocial":
if getattr(session, "type", "") == "blueski":
if self.showing == False:
dlg = wx.TextEntryDialog(None, _("Write your reply:"), _("Reply"))
if dlg.ShowModal() == wx.ID_OK:
@@ -732,7 +741,7 @@ class Controller(object):
else:
dlg.Destroy()
return
from wxUI.dialogs.atprotosocial.postDialogs import Post as ATPostDialog
from wxUI.dialogs.blueski.postDialogs import Post as ATPostDialog
dlg = ATPostDialog(caption=_("Reply"))
if dlg.ShowModal() == wx.ID_OK:
text, files, cw_text, langs = dlg.get_payload()
@@ -774,7 +783,7 @@ class Controller(object):
session = getattr(buffer, "session", None)
if not session:
return
if getattr(session, "type", "") == "atprotosocial":
if getattr(session, "type", "") == "blueski":
item_uri = None
if hasattr(buffer, "get_selected_item_id"):
item_uri = buffer.get_selected_item_id()
@@ -819,7 +828,7 @@ class Controller(object):
dlg.Destroy()
return
from wxUI.dialogs.atprotosocial.postDialogs import Post as ATPostDialog
from wxUI.dialogs.blueski.postDialogs import Post as ATPostDialog
dlg = ATPostDialog(caption=_("Quote post"))
if dlg.ShowModal() == wx.ID_OK:
text, files, cw_text, langs = dlg.get_payload()
@@ -865,7 +874,7 @@ class Controller(object):
buffer = self.get_current_buffer()
if hasattr(buffer, "add_to_favorites"): # Generic buffer method
return buffer.add_to_favorites()
elif buffer.session and buffer.session.KIND == "atprotosocial":
elif buffer.session and buffer.session.KIND == "blueski":
item_uri = buffer.get_selected_item_id()
if not item_uri:
output.speak(_("No item selected to like."), True)
@@ -894,7 +903,7 @@ class Controller(object):
buffer = self.get_current_buffer()
if hasattr(buffer, "remove_from_favorites"): # Generic buffer method
return buffer.remove_from_favorites()
elif buffer.session and buffer.session.KIND == "atprotosocial":
elif buffer.session and buffer.session.KIND == "blueski":
item_uri = buffer.get_selected_item_id()
if not item_uri:
output.speak(_("No item selected to unlike."), True)
@@ -1423,9 +1432,9 @@ class Controller(object):
def update_buffers(self):
for i in self.buffers[:]:
if i.session != None and i.session.is_logged == True:
# For ATProtoSocial, initial load is in session.start() or manual.
# For Blueski, initial load is in session.start() or manual.
# Periodic updates would need a separate timer or manual refresh via update_buffer.
if i.session.KIND != "atprotosocial":
if i.session.KIND != "blueski":
try:
i.start_stream(mandatory=True) # This is likely for streaming connections or timed polling within buffer
except Exception as err:
@@ -1444,7 +1453,7 @@ class Controller(object):
async def do_update():
new_ids = []
try:
if session.KIND == "atprotosocial":
if session.KIND == "blueski":
if bf.name == f"{session.label} Home": # Assuming buffer name indicates type
# Its panel's load_initial_posts calls session.fetch_home_timeline
if hasattr(bf, "load_initial_posts"): # Generic for timeline panels
@@ -1462,7 +1471,7 @@ class Controller(object):
await bf.load_initial_users(limit=config.app["app-settings"].get("items_per_request", 30))
new_ids = [u.get("did") for u in getattr(bf, "user_list_data", []) if isinstance(u,dict)]
else:
if hasattr(bf, "start_stream"): # Fallback for non-ATProtoSocial panels or unhandled types
if hasattr(bf, "start_stream"): # Fallback for non-Blueski panels or unhandled types
count = bf.start_stream(mandatory=True, avoid_autoreading=True)
if count is not None: new_ids = [str(x) for x in range(count)] # Dummy IDs for count
else:
@@ -1506,14 +1515,14 @@ class Controller(object):
# e.g., bf.pagination_cursor or bf.older_items_cursor
# This cursor should be set by the result of previous fetch_..._timeline(new_only=False) calls.
# For ATProtoSocial, session methods like fetch_home_timeline store their own cursor (e.g., session.home_timeline_cursor)
# For Blueski, session methods like fetch_home_timeline store their own cursor (e.g., session.home_timeline_cursor)
# The panel (bf) itself should manage its own cursor for "load more"
current_cursor = None
can_load_more_natively = False
if session.KIND == "atprotosocial":
if hasattr(bf, "load_more_posts"): # For ATProtoSocialUserTimelinePanel & ATProtoSocialHomeTimelinePanel
if session.KIND == "blueski":
if hasattr(bf, "load_more_posts"): # For BlueskiUserTimelinePanel & BlueskiHomeTimelinePanel
can_load_more_natively = True
if hasattr(bf, "load_more_posts"):
can_load_more_natively = True
@@ -1530,7 +1539,7 @@ class Controller(object):
else:
output.speak(_(u"This buffer does not support loading more items in this way."), True)
return
else: # For other non-ATProtoSocial session types
else: # For other non-Blueski session types
if hasattr(bf, "get_more_items"):
return bf.get_more_items()
else:
@@ -1541,7 +1550,7 @@ class Controller(object):
async def do_load_more():
try:
if session.KIND == "atprotosocial":
if session.KIND == "blueski":
if hasattr(bf, "load_more_posts"):
await bf.load_more_posts(limit=config.app["app-settings"].get("items_per_request", 20))
elif hasattr(bf, "load_more_users"):
@@ -1664,7 +1673,7 @@ class Controller(object):
if handler and hasattr(handler, 'user_details'):
# The handler's user_details method is responsible for extracting context
# (e.g., selected user) from the buffer and displaying the profile.
# For ATProtoSocial, handler.user_details calls the ShowUserProfileDialog.
# For Blueski, handler.user_details calls the ShowUserProfileDialog.
# It's an async method, so needs to be called appropriately.
async def _show_details():
await handler.user_details(buffer)

View File

@@ -48,7 +48,7 @@ class Handler(object):
addAlias=_("Add a&lias"),
addToList=None,
removeFromList=None,
details=_("Show user profile"),
details=_("S&how user profile"),
favs=None,
# In buffer Menu.
community_timeline =_("Create c&ommunity timeline"),

View File

@@ -2,6 +2,7 @@
import os
import re
import wx
import logging
import widgetUtils
import config
import output
@@ -14,6 +15,8 @@ from wxUI.dialogs.mastodon import postDialogs
from extra.autocompletionUsers import completion
from . import userList
log = logging.getLogger("controller.mastodon.messages")
def character_count(post_text, post_cw, character_limit=500):
# We will use text for counting character limit only.
full_text = post_text+post_cw
@@ -262,6 +265,108 @@ class post(messages.basicMessage):
visibility_setting = visibility_settings.index(setting)
self.message.visibility.SetSelection(setting)
class editPost(post):
def __init__(self, session, item, title, caption, *args, **kwargs):
""" Initialize edit dialog with existing post data.
Note: Per Mastodon API, visibility and language cannot be changed when editing.
These fields will be displayed but disabled in the UI.
"""
# Extract text from post
if item.reblog != None:
item = item.reblog
text = item.content
# Remove HTML tags from content
import re
text = re.sub('<[^<]+?>', '', text)
# Initialize parent class
super(editPost, self).__init__(session, title, caption, text=text, *args, **kwargs)
# Store the post ID for editing
self.post_id = item.id
# Set visibility (read-only, cannot be changed)
visibility_settings = dict(public=0, unlisted=1, private=2, direct=3)
self.message.visibility.SetSelection(visibility_settings.get(item.visibility, 0))
self.message.visibility.Enable(False) # Disable as it cannot be edited
# Set language (read-only, cannot be changed)
if item.language:
self.set_language(item.language)
self.message.language.Enable(False) # Disable as it cannot be edited
# Set sensitive content and spoiler
if item.sensitive:
self.message.sensitive.SetValue(True)
if item.spoiler_text:
self.message.spoiler.ChangeValue(item.spoiler_text)
self.message.on_sensitivity_changed()
# Load existing poll (if any)
# Note: You cannot have both media and a poll, so check poll first
if hasattr(item, 'poll') and item.poll is not None:
log.debug("Loading existing poll for post {}".format(self.post_id))
poll = item.poll
# Extract poll options (just the text, not the votes)
poll_options = [option.title for option in poll.options]
# Calculate expires_in based on current time and expires_at
# For editing, we need to provide a new expiration time
# Since we can't get the original expires_in, use a default or let user configure
# For now, use 1 day (86400 seconds) as default
expires_in = 86400
if hasattr(poll, 'expires_at') and poll.expires_at and not poll.expired:
# Calculate remaining time if poll hasn't expired
from dateutil import parser as date_parser
import datetime
try:
expires_at = poll.expires_at
if isinstance(expires_at, str):
expires_at = date_parser.parse(expires_at)
now = datetime.datetime.now(datetime.timezone.utc)
remaining = (expires_at - now).total_seconds()
if remaining > 0:
expires_in = int(remaining)
except Exception as e:
log.warning("Could not calculate poll expiration: {}".format(e))
poll_info = {
"type": "poll",
"file": "",
"description": _("Poll with {} options").format(len(poll_options)),
"options": poll_options,
"expires_in": expires_in,
"multiple": poll.multiple if hasattr(poll, 'multiple') else False,
"hide_totals": poll.voters_count == 0 if hasattr(poll, 'voters_count') else False
}
self.attachments.append(poll_info)
self.message.add_item(item=[poll_info["file"], poll_info["type"], poll_info["description"]])
log.debug("Loaded poll with {} options. WARNING: Editing will reset all votes!".format(len(poll_options)))
# Load existing media attachments (only if no poll)
elif hasattr(item, 'media_attachments'):
log.debug("Loading existing media attachments for post {}".format(self.post_id))
log.debug("Item has media_attachments attribute, count: {}".format(len(item.media_attachments)))
if len(item.media_attachments) > 0:
for media in item.media_attachments:
log.debug("Processing media: id={}, type={}, url={}".format(media.id, media.type, media.url))
media_info = {
"id": media.id, # Keep the existing media ID
"type": media.type,
"file": media.url, # URL of existing media
"description": media.description or ""
}
# Include focus point if available
if hasattr(media, 'meta') and media.meta and 'focus' in media.meta:
focus = media.meta['focus']
media_info["focus"] = (focus.get('x'), focus.get('y'))
log.debug("Added focus point: {}".format(media_info["focus"]))
self.attachments.append(media_info)
# Display in the attachment list
display_name = media.url.split('/')[-1]
log.debug("Adding item to UI: name={}, type={}, desc={}".format(display_name, media.type, media.description or ""))
self.message.add_item(item=[display_name, media.type, media.description or ""])
log.debug("Total attachments loaded: {}".format(len(self.attachments)))
else:
log.debug("media_attachments list is empty")
else:
log.debug("Item has no poll or media attachments")
# Update text processor to reflect the loaded content
self.text_processor()
class viewPost(post):
def __init__(self, session, post, offset_hours=0, date="", item_url=""):
self.session = session