mirror of
https://github.com/MCV-Software/TWBlue.git
synced 2026-03-06 17:37:33 +01:00
Commit
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
[atprotosocial]
|
||||
[blueski]
|
||||
handle = string(default="")
|
||||
app_password = string(default="")
|
||||
did = string(default="")
|
||||
@@ -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:
|
||||
@@ -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).")
|
||||
@@ -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).")
|
||||
@@ -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).")
|
||||
@@ -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).")
|
||||
@@ -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).")
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,8 +13,8 @@ class autocompletionManageDialog(widgetUtils.BaseDialog):
|
||||
self.users = widgets.list(panel, _(u"Username"), _(u"Name"), style=wx.LC_REPORT)
|
||||
sizer.Add(label, 0, wx.ALL, 5)
|
||||
sizer.Add(self.users.list, 0, wx.ALL, 5)
|
||||
self.add = wx.Button(panel, -1, _(u"Add user"))
|
||||
self.remove = wx.Button(panel, -1, _(u"Remove user"))
|
||||
self.add = wx.Button(panel, -1, _(u"&Add user"))
|
||||
self.remove = wx.Button(panel, -1, _(u"&Remove user"))
|
||||
optionsBox = wx.BoxSizer(wx.HORIZONTAL)
|
||||
optionsBox.Add(self.add, 0, wx.ALL, 5)
|
||||
optionsBox.Add(self.remove, 0, wx.ALL, 5)
|
||||
|
||||
@@ -23,6 +23,7 @@ url = string(default="control+win+b")
|
||||
go_home = string(default="control+win+home")
|
||||
go_end = string(default="control+win+end")
|
||||
delete = string(default="control+win+delete")
|
||||
edit_post = string(default="")
|
||||
clear_buffer = string(default="control+win+shift+delete")
|
||||
repeat_item = string(default="control+win+space")
|
||||
copy_to_clipboard = string(default="control+win+shift+c")
|
||||
|
||||
@@ -33,6 +33,7 @@ go_page_up = string(default="control+win+pageup")
|
||||
go_page_down = string(default="control+win+pagedown")
|
||||
update_profile = string(default="control+win+shift+p")
|
||||
delete = string(default="control+win+delete")
|
||||
edit_post = string(default="")
|
||||
clear_buffer = string(default="control+win+shift+delete")
|
||||
repeat_item = string(default="control+win+space")
|
||||
copy_to_clipboard = string(default="control+win+shift+c")
|
||||
|
||||
@@ -33,6 +33,7 @@ go_page_up = string(default="control+win+pageup")
|
||||
go_page_down = string(default="control+win+pagedown")
|
||||
update_profile = string(default="alt+win+p")
|
||||
delete = string(default="alt+win+delete")
|
||||
edit_post = string(default="")
|
||||
clear_buffer = string(default="alt+win+shift+delete")
|
||||
repeat_item = string(default="alt+win+space")
|
||||
copy_to_clipboard = string(default="alt+win+shift+c")
|
||||
|
||||
@@ -33,6 +33,7 @@ go_page_up = string(default="control+win+pageup")
|
||||
go_page_down = string(default="control+win+pagedown")
|
||||
update_profile = string(default="alt+win+p")
|
||||
delete = string(default="alt+win+delete")
|
||||
edit_post = string(default="")
|
||||
clear_buffer = string(default="alt+win+shift+delete")
|
||||
repeat_item = string(default="control+alt+win+space")
|
||||
copy_to_clipboard = string(default="alt+win+shift+c")
|
||||
|
||||
@@ -34,6 +34,7 @@ go_page_up = string(default="control+win+pageup")
|
||||
go_page_down = string(default="control+win+pagedown")
|
||||
update_profile = string(default="alt+win+p")
|
||||
delete = string(default="control+win+delete")
|
||||
edit_post = string(default="")
|
||||
clear_buffer = string(default="control+win+shift+delete")
|
||||
repeat_item = string(default="control+win+space")
|
||||
copy_to_clipboard = string(default="control+win+shift+c")
|
||||
|
||||
Binary file not shown.
@@ -1,22 +1,23 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) 2019 ORGANIZATION
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
|
||||
# zvonimir stanecic <zvonimirek222@yandex.com>, 2023.
|
||||
# zvonimir stanecic <zvonimirek222@yandex.com>, 2023, 2025.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Tw Blue 0.80\n"
|
||||
"Report-Msgid-Bugs-To: manuel@manuelcortez.net\n"
|
||||
"POT-Creation-Date: 2025-04-13 01:18+0000\n"
|
||||
"PO-Revision-Date: 2023-04-21 07:45+0000\n"
|
||||
"PO-Revision-Date: 2025-08-10 16:08+0000\n"
|
||||
"Last-Translator: zvonimir stanecic <zvonimirek222@yandex.com>\n"
|
||||
"Language-Team: Polish <https://weblate.mcvsoftware.com/projects/twblue/"
|
||||
"twblue/pl/>\n"
|
||||
"Language: pl\n"
|
||||
"Language-Team: Polish "
|
||||
"<https://weblate.mcvsoftware.com/projects/twblue/twblue/pl/>\n"
|
||||
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && "
|
||||
"(n%100<10 || n%100>=20) ? 1 : 2;\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
|
||||
"|| n%100>=20) ? 1 : 2;\n"
|
||||
"X-Generator: Weblate 5.10.4\n"
|
||||
"Generated-By: Babel 2.17.0\n"
|
||||
|
||||
#: languageHandler.py:61
|
||||
@@ -101,7 +102,7 @@ msgstr "Domyślne dla użytkownika"
|
||||
#: main.py:105
|
||||
#, fuzzy
|
||||
msgid "https://twblue.mcvsoftware.com/donate"
|
||||
msgstr "https://twblue.es/donate"
|
||||
msgstr "https://twblue.mcvsoftware.com/donate"
|
||||
|
||||
#: main.py:118
|
||||
#, python-brace-format
|
||||
@@ -246,9 +247,8 @@ msgid "Following for {}"
|
||||
msgstr "Śledzący użytkownika {}"
|
||||
|
||||
#: controller/messages.py:18
|
||||
#, fuzzy
|
||||
msgid "Translated"
|
||||
msgstr "&Przetłumacz"
|
||||
msgstr "Przetłumaczono"
|
||||
|
||||
#: controller/settings.py:60
|
||||
msgid "System default"
|
||||
@@ -540,9 +540,8 @@ msgid "There are no more items in this buffer."
|
||||
msgstr "W tym buforze nie ma więcej elementów."
|
||||
|
||||
#: controller/mastodon/handler.py:30 wxUI/dialogs/mastodon/updateProfile.py:35
|
||||
#, fuzzy
|
||||
msgid "Update Profile"
|
||||
msgstr "&Edytuj profil"
|
||||
msgstr "Zaktualizuj profil"
|
||||
|
||||
#: controller/mastodon/handler.py:31 wxUI/dialogs/mastodon/search.py:10
|
||||
#: wxUI/view.py:19
|
||||
@@ -615,13 +614,12 @@ msgid "Add a&lias"
|
||||
msgstr "Dodaj a&lias"
|
||||
|
||||
#: controller/mastodon/handler.py:51
|
||||
#, fuzzy
|
||||
msgid "Show user profile"
|
||||
msgstr "&Pokaż profil użytkownika"
|
||||
msgstr "Pokaż profil użytkownika"
|
||||
|
||||
#: controller/mastodon/handler.py:54
|
||||
msgid "Create c&ommunity timeline"
|
||||
msgstr ""
|
||||
msgstr "Stwórz &oś czasu społeczności"
|
||||
|
||||
#: controller/mastodon/handler.py:55 wxUI/view.py:57
|
||||
msgid "Create a &filter"
|
||||
@@ -647,10 +645,9 @@ msgstr "Wyszukiwanie {}"
|
||||
|
||||
#: controller/mastodon/handler.py:111
|
||||
msgid "Communities"
|
||||
msgstr ""
|
||||
msgstr "Społeczności"
|
||||
|
||||
#: controller/mastodon/handler.py:114
|
||||
#, fuzzy
|
||||
msgid "federated"
|
||||
msgstr "federowana"
|
||||
|
||||
@@ -4864,4 +4861,3 @@ msgstr "Dodatki"
|
||||
|
||||
#~ msgid "DeepL API Key: "
|
||||
#~ msgstr ""
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from pubsub import pub
|
||||
from controller import settings
|
||||
from sessions.mastodon import session as MastodonSession
|
||||
from sessions.gotosocial import session as GotosocialSession
|
||||
from sessions.atprotosocial import session as ATProtoSocialSession # Import ATProtoSocial session
|
||||
from sessions.blueski import session as BlueskiSession # Import Blueski session
|
||||
from . import manager
|
||||
from . import wxUI as view
|
||||
|
||||
@@ -74,21 +74,37 @@ class sessionManagerController(object):
|
||||
if config_test["mastodon"]["instance"] != "" and config_test["mastodon"]["access_token"] != "": # Basic validation
|
||||
sessionsList.append(name)
|
||||
self.sessions.append(dict(type=config_test["mastodon"].get("type", "mastodon"), id=i))
|
||||
elif config_test.get("atprotosocial") != None: # Check for ATProtoSocial config
|
||||
handle = config_test["atprotosocial"].get("handle")
|
||||
did = config_test["atprotosocial"].get("did") # DID confirms it was authorized
|
||||
elif config_test.get("blueski") != None: # Check for Blueski config
|
||||
handle = config_test["blueski"].get("handle")
|
||||
did = config_test["blueski"].get("did") # DID confirms it was authorized
|
||||
if handle and did:
|
||||
name = _("{handle} (Bluesky)").format(handle=handle)
|
||||
sessionsList.append(name)
|
||||
self.sessions.append(dict(type="atprotosocial", id=i))
|
||||
self.sessions.append(dict(type="blueski", id=i))
|
||||
else: # Incomplete config, might be an old attempt or error
|
||||
log.warning(f"Incomplete ATProtoSocial session config found for {i}, skipping.")
|
||||
log.warning(f"Incomplete Blueski session config found for {i}, skipping.")
|
||||
# Optionally delete malformed config here too
|
||||
try:
|
||||
log.debug("Deleting incomplete ATProtoSocial session %s" % (i,))
|
||||
log.debug("Deleting incomplete Blueski session %s" % (i,))
|
||||
shutil.rmtree(os.path.join(paths.config_path(), i))
|
||||
except Exception as e:
|
||||
log.exception(f"Error deleting incomplete ATProtoSocial session {i}: {e}")
|
||||
log.exception(f"Error deleting incomplete Blueski session {i}: {e}")
|
||||
continue
|
||||
elif config_test.get("atprotosocial") != None: # Legacy config namespace
|
||||
handle = config_test["atprotosocial"].get("handle")
|
||||
did = config_test["atprotosocial"].get("did")
|
||||
if handle and did:
|
||||
name = _("{handle} (Bluesky)").format(handle=handle)
|
||||
sessionsList.append(name)
|
||||
self.sessions.append(dict(type="blueski", id=i))
|
||||
else: # Incomplete config, might be an old attempt or error
|
||||
log.warning(f"Incomplete Blueski session config found for {i}, skipping.")
|
||||
# Optionally delete malformed config here too
|
||||
try:
|
||||
log.debug("Deleting incomplete Blueski session %s" % (i,))
|
||||
shutil.rmtree(os.path.join(paths.config_path(), i))
|
||||
except Exception as e:
|
||||
log.exception(f"Error deleting incomplete Blueski session {i}: {e}")
|
||||
continue
|
||||
else: # Unknown or other session type not explicitly handled here for display
|
||||
try:
|
||||
@@ -117,14 +133,14 @@ class sessionManagerController(object):
|
||||
s = MastodonSession.Session(i.get("id"))
|
||||
elif i.get("type") == "gotosocial":
|
||||
s = GotosocialSession.Session(i.get("id"))
|
||||
elif i.get("type") == "atprotosocial": # Handle ATProtoSocial session type
|
||||
s = ATProtoSocialSession.Session(i.get("id"))
|
||||
elif i.get("type") == "blueski": # Handle Blueski session type
|
||||
s = BlueskiSession.Session(i.get("id"))
|
||||
else:
|
||||
log.warning(f"Unknown session type '{i.get('type')}' for ID {i.get('id')}. Skipping.")
|
||||
continue
|
||||
|
||||
s.get_configuration() # Load per-session configuration
|
||||
# For ATProtoSocial, this loads from its specific config file.
|
||||
# For Blueski, this loads from its specific config file.
|
||||
|
||||
# Login is now primarily handled by session.start() via mainController,
|
||||
# which calls _ensure_dependencies_ready().
|
||||
@@ -132,19 +148,19 @@ class sessionManagerController(object):
|
||||
# We'll rely on the mainController to call session.start() which handles login.
|
||||
# if i.get("id") not in config.app["sessions"]["ignored_sessions"]:
|
||||
# try:
|
||||
# # For ATProtoSocial, login is async and handled by session.start()
|
||||
# # For Blueski, login is async and handled by session.start()
|
||||
# # if not s.is_ready(): # Only attempt login if not already ready
|
||||
# # log.info(f"Session {s.uid} ({s.kind}) not ready, login will be attempted by start().")
|
||||
# pass
|
||||
# except Exception as e:
|
||||
# log.exception(f"Exception during pre-emptive login check for session {s.uid} ({s.kind}).")
|
||||
# continue
|
||||
# Try to auto-login for ATProtoSocial so the app starts with buffers ready
|
||||
# Try to auto-login for Blueski so the app starts with buffers ready
|
||||
try:
|
||||
if i.get("type") == "atprotosocial":
|
||||
if i.get("type") == "blueski":
|
||||
s.login()
|
||||
except Exception:
|
||||
log.exception("Auto-login failed for ATProtoSocial session %s", i.get("id"))
|
||||
log.exception("Auto-login failed for Blueski session %s", i.get("id"))
|
||||
|
||||
sessions.sessions[i.get("id")] = s # Add to global session store
|
||||
self.new_sessions[i.get("id")] = s # Track as a new session for this manager instance
|
||||
@@ -162,8 +178,8 @@ class sessionManagerController(object):
|
||||
|
||||
if type == "mastodon":
|
||||
s = MastodonSession.Session(location)
|
||||
elif type == "atprotosocial":
|
||||
s = ATProtoSocialSession.Session(location)
|
||||
elif type == "blueski":
|
||||
s = BlueskiSession.Session(location)
|
||||
# Add other session types here if needed (e.g., gotosocial)
|
||||
# elif type == "gotosocial":
|
||||
# s = GotosocialSession.Session(location)
|
||||
|
||||
@@ -54,8 +54,8 @@ class sessionManagerWindow(wx.Dialog):
|
||||
mastodon = menu.Append(wx.ID_ANY, _("Mastodon"))
|
||||
menu.Bind(wx.EVT_MENU, self.on_new_mastodon_account, mastodon)
|
||||
|
||||
atprotosocial = menu.Append(wx.ID_ANY, _("ATProtoSocial (Bluesky)"))
|
||||
menu.Bind(wx.EVT_MENU, self.on_new_atprotosocial_account, atprotosocial)
|
||||
blueski = menu.Append(wx.ID_ANY, _("Blueski (Bluesky)"))
|
||||
menu.Bind(wx.EVT_MENU, self.on_new_blueski_account, blueski)
|
||||
|
||||
self.PopupMenu(menu, self.new.GetPosition())
|
||||
|
||||
@@ -66,12 +66,12 @@ class sessionManagerWindow(wx.Dialog):
|
||||
if response == wx.ID_YES:
|
||||
pub.sendMessage("sessionmanager.new_account", type="mastodon")
|
||||
|
||||
def on_new_atprotosocial_account(self, *args, **kwargs):
|
||||
dlg = wx.MessageDialog(self, _("You will be prompted for your ATProtoSocial (Bluesky) data (user handle and App Password) to authorize TWBlue. Would you like to authorize your account now?"), _(u"ATProtoSocial Authorization"), wx.YES_NO)
|
||||
def on_new_blueski_account(self, *args, **kwargs):
|
||||
dlg = wx.MessageDialog(self, _("You will be prompted for your Blueski (Bluesky) data (user handle and App Password) to authorize TWBlue. Would you like to authorize your account now?"), _(u"Blueski Authorization"), wx.YES_NO)
|
||||
response = dlg.ShowModal()
|
||||
dlg.Destroy()
|
||||
if response == wx.ID_YES:
|
||||
pub.sendMessage("sessionmanager.new_account", type="atprotosocial")
|
||||
pub.sendMessage("sessionmanager.new_account", type="blueski")
|
||||
|
||||
def add_new_session_to_list(self):
|
||||
total = self.list.get_count()
|
||||
|
||||
@@ -9,7 +9,7 @@ from approve.translation import translate as _
|
||||
from approve.util import parse_iso_datetime # For parsing ISO timestamps
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from approve.sessions.atprotosocial.session import Session as ATProtoSocialSession
|
||||
from approve.sessions.blueski.session import Session as BlueskiSession
|
||||
from atproto.xrpc_client import models # For type hinting ATProto models
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -21,19 +21,19 @@ SUPPORTED_LANG_CHOICES_COMPOSE = {
|
||||
}
|
||||
|
||||
|
||||
class ATProtoSocialCompose:
|
||||
class BlueskiCompose:
|
||||
MAX_CHARS = 300
|
||||
MAX_MEDIA_ATTACHMENTS = 4
|
||||
MAX_LANGUAGES = 3
|
||||
MAX_IMAGE_SIZE_BYTES = 1_000_000
|
||||
|
||||
def __init__(self, session: ATProtoSocialSession) -> None:
|
||||
def __init__(self, session: BlueskiSession) -> None:
|
||||
self.session = session
|
||||
self.supported_media_types: list[str] = ["image/jpeg", "image/png"]
|
||||
self.max_image_size_bytes: int = self.MAX_IMAGE_SIZE_BYTES
|
||||
|
||||
def get_panel_configuration(self) -> dict[str, Any]:
|
||||
"""Returns configuration for the compose panel specific to ATProtoSocial."""
|
||||
"""Returns configuration for the compose panel specific to Blueski."""
|
||||
return {
|
||||
"max_chars": self.MAX_CHARS,
|
||||
"max_media_attachments": self.MAX_MEDIA_ATTACHMENTS,
|
||||
@@ -206,7 +206,7 @@ class ATProtoSocialCompose:
|
||||
|
||||
Args:
|
||||
notif_data: A dictionary representing the notification,
|
||||
typically from ATProtoSocialSession._handle_*_notification methods
|
||||
typically from BlueskiSession._handle_*_notification methods
|
||||
which create an approve.notifications.Notification object and then
|
||||
convert it to dict or pass relevant parts.
|
||||
Expected keys: 'title', 'body', 'author_name', 'timestamp_dt', 'kind'.
|
||||
@@ -10,7 +10,7 @@ from sessions import session_exceptions as Exceptions
|
||||
import output
|
||||
import application
|
||||
|
||||
log = logging.getLogger("sessions.atprotosocialSession")
|
||||
log = logging.getLogger("sessions.blueskiSession")
|
||||
|
||||
# Optional import of atproto. Code handles absence gracefully.
|
||||
try:
|
||||
@@ -27,26 +27,45 @@ class Session(base.baseSession):
|
||||
"""
|
||||
|
||||
name = "Bluesky"
|
||||
KIND = "atprotosocial"
|
||||
KIND = "blueski"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Session, self).__init__(*args, **kwargs)
|
||||
self.config_spec = "atprotosocial.defaults"
|
||||
self.type = "atprotosocial"
|
||||
self.config_spec = "blueski.defaults"
|
||||
self.type = "blueski"
|
||||
self.char_limit = 300
|
||||
self.api = None
|
||||
|
||||
def _ensure_settings_namespace(self) -> None:
|
||||
"""Migrate legacy atprotosocial settings to blueski namespace."""
|
||||
try:
|
||||
if not self.settings:
|
||||
return
|
||||
if self.settings.get("blueski") is None and self.settings.get("atprotosocial") is not None:
|
||||
self.settings["blueski"] = dict(self.settings["atprotosocial"])
|
||||
try:
|
||||
del self.settings["atprotosocial"]
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.settings.write()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
log.exception("Failed to migrate legacy Blueski settings")
|
||||
|
||||
def get_name(self):
|
||||
"""Return a human-friendly, stable account name for UI.
|
||||
|
||||
Prefer the user's handle if available so accounts are uniquely
|
||||
identifiable, falling back to a generic network name otherwise.
|
||||
"""
|
||||
self._ensure_settings_namespace()
|
||||
try:
|
||||
# Prefer runtime DB, then persisted settings, then SDK client
|
||||
handle = (
|
||||
self.db.get("user_name")
|
||||
or (self.settings and self.settings.get("atprotosocial", {}).get("handle"))
|
||||
or (self.settings and self.settings.get("blueski", {}).get("handle"))
|
||||
or (getattr(getattr(self, "api", None), "me", None) and self.api.me.handle)
|
||||
)
|
||||
if handle:
|
||||
@@ -65,11 +84,12 @@ class Session(base.baseSession):
|
||||
return self.api
|
||||
|
||||
def login(self, verify_credentials=True):
|
||||
if self.settings.get("atprotosocial") is None:
|
||||
self._ensure_settings_namespace()
|
||||
if self.settings.get("blueski") is None:
|
||||
raise Exceptions.RequireCredentialsSessionError
|
||||
handle = self.settings["atprotosocial"].get("handle")
|
||||
app_password = self.settings["atprotosocial"].get("app_password")
|
||||
session_string = self.settings["atprotosocial"].get("session_string")
|
||||
handle = self.settings["blueski"].get("handle")
|
||||
app_password = self.settings["blueski"].get("app_password")
|
||||
session_string = self.settings["blueski"].get("session_string")
|
||||
if not handle or (not app_password and not session_string):
|
||||
self.logged = False
|
||||
raise Exceptions.RequireCredentialsSessionError
|
||||
@@ -100,10 +120,10 @@ class Session(base.baseSession):
|
||||
self.db["user_name"] = api.me.handle
|
||||
self.db["user_id"] = api.me.did
|
||||
# Persist DID in settings for session manager display
|
||||
self.settings["atprotosocial"]["did"] = api.me.did
|
||||
self.settings["blueski"]["did"] = api.me.did
|
||||
# Export session for future reuse
|
||||
try:
|
||||
self.settings["atprotosocial"]["session_string"] = api.export_session_string()
|
||||
self.settings["blueski"]["session_string"] = api.export_session_string()
|
||||
except Exception:
|
||||
pass
|
||||
self.settings.write()
|
||||
@@ -114,6 +134,7 @@ class Session(base.baseSession):
|
||||
self.logged = False
|
||||
|
||||
def authorise(self):
|
||||
self._ensure_settings_namespace()
|
||||
if self.logged:
|
||||
raise Exceptions.AlreadyAuthorisedError("Already authorised.")
|
||||
# Ask for handle
|
||||
@@ -141,8 +162,8 @@ class Session(base.baseSession):
|
||||
# Create session folder and config, then attempt login
|
||||
self.create_session_folder()
|
||||
self.get_configuration()
|
||||
self.settings["atprotosocial"]["handle"] = handle
|
||||
self.settings["atprotosocial"]["app_password"] = app_password
|
||||
self.settings["blueski"]["handle"] = handle
|
||||
self.settings["blueski"]["app_password"] = app_password
|
||||
self.settings.write()
|
||||
try:
|
||||
self.login()
|
||||
@@ -159,7 +180,8 @@ class Session(base.baseSession):
|
||||
|
||||
def get_message_url(self, message_id, context=None):
|
||||
# message_id may be full at:// URI or rkey
|
||||
handle = self.db.get("user_name") or self.settings["atprotosocial"].get("handle", "")
|
||||
self._ensure_settings_namespace()
|
||||
handle = self.db.get("user_name") or self.settings["blueski"].get("handle", "")
|
||||
rkey = message_id
|
||||
if isinstance(message_id, str) and message_id.startswith("at://"):
|
||||
parts = message_id.split("/")
|
||||
@@ -169,6 +191,7 @@ class Session(base.baseSession):
|
||||
def send_message(self, message, files=None, reply_to=None, cw_text=None, is_sensitive=False, **kwargs):
|
||||
if not self.logged:
|
||||
raise Exceptions.NotLoggedSessionError("You are not logged in yet.")
|
||||
self._ensure_settings_namespace()
|
||||
try:
|
||||
api = self._ensure_client()
|
||||
# Basic text-only post for now. Attachments and CW can be extended later.
|
||||
@@ -273,8 +296,8 @@ class Session(base.baseSession):
|
||||
# Accept full web URL and try to resolve via get_post_thread below
|
||||
return identifier
|
||||
# Accept bare rkey case by constructing a guess using own handle
|
||||
handle = self.db.get("user_name") or self.settings["atprotosocial"].get("handle")
|
||||
did = self.db.get("user_id") or self.settings["atprotosocial"].get("did")
|
||||
handle = self.db.get("user_name") or self.settings["blueski"].get("handle")
|
||||
did = self.db.get("user_id") or self.settings["blueski"].get("did")
|
||||
if handle and did and len(identifier) in (13, 14, 15):
|
||||
# rkey length is typically ~13 chars base32
|
||||
return f"at://{did}/app.bsky.feed.post/{identifier}"
|
||||
@@ -5,17 +5,17 @@ import logging
|
||||
from typing import TYPE_CHECKING, Any, Callable, Coroutine
|
||||
|
||||
if TYPE_CHECKING:
|
||||
fromapprove.sessions.atprotosocial.session import Session as ATProtoSocialSession
|
||||
fromapprove.sessions.blueski.session import Session as BlueskiSession
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ATProtoSocial (Bluesky) uses a Firehose model for streaming.
|
||||
# Blueski (Bluesky) uses a Firehose model for streaming.
|
||||
# This typically involves connecting to a WebSocket endpoint and receiving events.
|
||||
# The atproto SDK provides tools for this.
|
||||
|
||||
|
||||
class ATProtoSocialStreaming:
|
||||
def __init__(self, session: ATProtoSocialSession, stream_type: str, params: dict[str, Any] | None = None) -> None:
|
||||
class BlueskiStreaming:
|
||||
def __init__(self, session: BlueskiSession, stream_type: str, params: dict[str, Any] | None = None) -> None:
|
||||
self.session = session
|
||||
self.stream_type = stream_type # e.g., 'user', 'public', 'hashtag' - will need mapping to Firehose concepts
|
||||
self.params = params or {}
|
||||
@@ -30,19 +30,19 @@ class ATProtoSocialStreaming:
|
||||
# or using a more specific subscription if available for user-level events.
|
||||
|
||||
async def _connect(self) -> None:
|
||||
"""Internal method to connect to the ATProtoSocial Firehose."""
|
||||
"""Internal method to connect to the Blueski Firehose."""
|
||||
# from atproto import AsyncClient
|
||||
# from atproto.firehose import FirehoseSubscribeReposClient, parse_subscribe_repos_message
|
||||
# from atproto.xrpc_client.models import get_or_create, ids, models
|
||||
|
||||
logger.info(f"ATProtoSocial streaming: Connecting to Firehose for user {self.session.user_id}, stream type {self.stream_type}")
|
||||
logger.info(f"Blueski streaming: Connecting to Firehose for user {self.session.user_id}, stream type {self.stream_type}")
|
||||
self._should_stop = False
|
||||
|
||||
try:
|
||||
# TODO: Replace with actual atproto SDK usage
|
||||
# client = self.session.util.get_client() # Get authenticated client from session utils
|
||||
# if not client or not client.me: # Check if client is authenticated
|
||||
# logger.error("ATProtoSocial client not authenticated. Cannot start Firehose.")
|
||||
# logger.error("Blueski client not authenticated. Cannot start Firehose.")
|
||||
# return
|
||||
|
||||
# self._firehose_client = FirehoseSubscribeReposClient(params=None, base_uri=self.session.api_base_url) # Adjust base_uri if needed
|
||||
@@ -77,7 +77,7 @@ class ATProtoSocialStreaming:
|
||||
# # # await self._handle_event("mention", event_data)
|
||||
|
||||
# # For now, we'll just log that a message was received
|
||||
# logger.debug(f"ATProtoSocial Firehose message received: {message.__class__.__name__}")
|
||||
# logger.debug(f"Blueski Firehose message received: {message.__class__.__name__}")
|
||||
|
||||
|
||||
# await self._firehose_client.start(on_message_handler)
|
||||
@@ -91,13 +91,13 @@ class ATProtoSocialStreaming:
|
||||
# mock_event = {"type": "placeholder_event", "data": {"text": "Hello from mock stream"}}
|
||||
# await self._handler(mock_event) # Call the registered handler
|
||||
|
||||
logger.info(f"ATProtoSocial streaming: Placeholder loop for {self.session.user_id} stopped.")
|
||||
logger.info(f"Blueski streaming: Placeholder loop for {self.session.user_id} stopped.")
|
||||
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info(f"ATProtoSocial streaming task for user {self.session.user_id} was cancelled.")
|
||||
logger.info(f"Blueski streaming task for user {self.session.user_id} was cancelled.")
|
||||
except Exception as e:
|
||||
logger.error(f"ATProtoSocial streaming error for user {self.session.user_id}: {e}", exc_info=True)
|
||||
logger.error(f"Blueski streaming error for user {self.session.user_id}: {e}", exc_info=True)
|
||||
# Optional: implement retry logic here or in the start_streaming method
|
||||
if not self._should_stop:
|
||||
await asyncio.sleep(30) # Wait before trying to reconnect (if auto-reconnect is desired)
|
||||
@@ -108,7 +108,7 @@ class ATProtoSocialStreaming:
|
||||
finally:
|
||||
# if self._firehose_client:
|
||||
# await self._firehose_client.stop()
|
||||
logger.info(f"ATProtoSocial streaming connection closed for user {self.session.user_id}.")
|
||||
logger.info(f"Blueski streaming connection closed for user {self.session.user_id}.")
|
||||
|
||||
|
||||
async def _handle_event(self, event_type: str, data: dict[str, Any]) -> None:
|
||||
@@ -118,31 +118,31 @@ class ATProtoSocialStreaming:
|
||||
if self._handler:
|
||||
try:
|
||||
# The data should be transformed into a common format expected by session.handle_streaming_event
|
||||
# This is where ATProtoSocial-specific event data is mapped to Approve's internal event structure.
|
||||
# For example, an ATProtoSocial 'mention' event needs to be structured similarly to
|
||||
# This is where Blueski-specific event data is mapped to Approve's internal event structure.
|
||||
# For example, an Blueski 'mention' event needs to be structured similarly to
|
||||
# how a Mastodon 'mention' event would be.
|
||||
await self.session.handle_streaming_event(event_type, data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling ATProtoSocial streaming event type {event_type}: {e}", exc_info=True)
|
||||
logger.error(f"Error handling Blueski streaming event type {event_type}: {e}", exc_info=True)
|
||||
else:
|
||||
logger.warning(f"ATProtoSocial streaming: No handler registered for session {self.session.user_id}, event: {event_type}")
|
||||
logger.warning(f"Blueski streaming: No handler registered for session {self.session.user_id}, event: {event_type}")
|
||||
|
||||
|
||||
def start_streaming(self, handler: Callable[[dict[str, Any]], Coroutine[Any, Any, None]]) -> None:
|
||||
"""Starts the streaming connection."""
|
||||
if self._connection_task and not self._connection_task.done():
|
||||
logger.warning(f"ATProtoSocial streaming already active for user {self.session.user_id}.")
|
||||
logger.warning(f"Blueski streaming already active for user {self.session.user_id}.")
|
||||
return
|
||||
|
||||
self._handler = handler # This handler is what session.py's handle_streaming_event calls
|
||||
self._should_stop = False
|
||||
logger.info(f"ATProtoSocial streaming: Starting for user {self.session.user_id}, type: {self.stream_type}")
|
||||
logger.info(f"Blueski streaming: Starting for user {self.session.user_id}, type: {self.stream_type}")
|
||||
self._connection_task = asyncio.create_task(self._connect())
|
||||
|
||||
|
||||
async def stop_streaming(self) -> None:
|
||||
"""Stops the streaming connection."""
|
||||
logger.info(f"ATProtoSocial streaming: Stopping for user {self.session.user_id}")
|
||||
logger.info(f"Blueski streaming: Stopping for user {self.session.user_id}")
|
||||
self._should_stop = True
|
||||
# if self._firehose_client: # Assuming the SDK has a stop method
|
||||
# await self._firehose_client.stop()
|
||||
@@ -153,10 +153,10 @@ class ATProtoSocialStreaming:
|
||||
try:
|
||||
await self._connection_task
|
||||
except asyncio.CancelledError:
|
||||
logger.info(f"ATProtoSocial streaming task successfully cancelled for {self.session.user_id}.")
|
||||
logger.info(f"Blueski streaming task successfully cancelled for {self.session.user_id}.")
|
||||
self._connection_task = None
|
||||
self._handler = None
|
||||
logger.info(f"ATProtoSocial streaming stopped for user {self.session.user_id}.")
|
||||
logger.info(f"Blueski streaming stopped for user {self.session.user_id}.")
|
||||
|
||||
def is_alive(self) -> bool:
|
||||
"""Checks if the streaming connection is currently active."""
|
||||
@@ -169,7 +169,7 @@ class ATProtoSocialStreaming:
|
||||
def get_params(self) -> dict[str, Any]:
|
||||
return self.params
|
||||
|
||||
# TODO: Add methods specific to ATProtoSocial streaming if necessary,
|
||||
# TODO: Add methods specific to Blueski streaming if necessary,
|
||||
# e.g., methods to modify subscription details on the fly if the API supports it.
|
||||
# For Bluesky Firehose, this might not be applicable as you usually connect and filter client-side.
|
||||
# However, if there were different Firehose endpoints (e.g., one for public posts, one for user-specific events),
|
||||
@@ -6,30 +6,30 @@ from typing import TYPE_CHECKING, Any
|
||||
fromapprove.translation import translate as _
|
||||
|
||||
if TYPE_CHECKING:
|
||||
fromapprove.sessions.atprotosocial.session import Session as ATProtoSocialSession
|
||||
fromapprove.sessions.blueski.session import Session as BlueskiSession
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ATProtoSocialTemplates:
|
||||
def __init__(self, session: ATProtoSocialSession) -> None:
|
||||
class BlueskiTemplates:
|
||||
def __init__(self, session: BlueskiSession) -> None:
|
||||
self.session = session
|
||||
|
||||
def get_template_data(self, template_name: str, context: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
"""
|
||||
Returns data required for rendering a specific template for ATProtoSocial.
|
||||
Returns data required for rendering a specific template for Blueski.
|
||||
This method would populate template variables based on the template name and context.
|
||||
"""
|
||||
base_data = {
|
||||
"session_kind": self.session.kind,
|
||||
"session_label": self.session.label,
|
||||
"user_id": self.session.user_id,
|
||||
# Add any other common data needed by ATProtoSocial templates
|
||||
# Add any other common data needed by Blueski templates
|
||||
}
|
||||
if context:
|
||||
base_data.update(context)
|
||||
|
||||
# TODO: Implement specific data fetching for different ATProtoSocial templates
|
||||
# TODO: Implement specific data fetching for different Blueski templates
|
||||
# Example:
|
||||
# if template_name == "profile_summary.html":
|
||||
# # profile_info = await self.session.util.get_my_profile_info() # Assuming such a method exists
|
||||
@@ -44,27 +44,27 @@ class ATProtoSocialTemplates:
|
||||
return base_data
|
||||
|
||||
def get_message_card_template(self) -> str:
|
||||
"""Returns the path to the message card template for ATProtoSocial."""
|
||||
# This template would define how a single ATProtoSocial post (or other message type)
|
||||
"""Returns the path to the message card template for Blueski."""
|
||||
# This template would define how a single Blueski post (or other message type)
|
||||
# is rendered in a list (e.g., in a timeline or search results).
|
||||
# return "sessions/atprotosocial/cards/message.html" # Example path
|
||||
# return "sessions/blueski/cards/message.html" # Example path
|
||||
return "sessions/generic/cards/message_generic.html" # Placeholder, use generic if no specific yet
|
||||
|
||||
def get_notification_template_map(self) -> dict[str, str]:
|
||||
"""
|
||||
Returns a map of ATProtoSocial notification types to their respective template paths.
|
||||
Returns a map of Blueski notification types to their respective template paths.
|
||||
"""
|
||||
# TODO: Define templates for different ATProtoSocial notification types
|
||||
# TODO: Define templates for different Blueski notification types
|
||||
# (e.g., mention, reply, new follower, like, repost).
|
||||
# The keys should match the notification types used internally by Approve
|
||||
# when processing ATProtoSocial events.
|
||||
# when processing Blueski events.
|
||||
# Example:
|
||||
# return {
|
||||
# "mention": "sessions/atprotosocial/notifications/mention.html",
|
||||
# "reply": "sessions/atprotosocial/notifications/reply.html",
|
||||
# "follow": "sessions/atprotosocial/notifications/follow.html",
|
||||
# "like": "sessions/atprotosocial/notifications/like.html", # Bluesky uses 'like'
|
||||
# "repost": "sessions/atprotosocial/notifications/repost.html", # Bluesky uses 'repost'
|
||||
# "mention": "sessions/blueski/notifications/mention.html",
|
||||
# "reply": "sessions/blueski/notifications/reply.html",
|
||||
# "follow": "sessions/blueski/notifications/follow.html",
|
||||
# "like": "sessions/blueski/notifications/like.html", # Bluesky uses 'like'
|
||||
# "repost": "sessions/blueski/notifications/repost.html", # Bluesky uses 'repost'
|
||||
# # ... other notification types
|
||||
# }
|
||||
# Using generic templates as placeholders:
|
||||
@@ -77,37 +77,37 @@ class ATProtoSocialTemplates:
|
||||
}
|
||||
|
||||
def get_settings_template(self) -> str | None:
|
||||
"""Returns the path to the settings template for ATProtoSocial, if any."""
|
||||
# This template would be used to render ATProtoSocial-specific settings in the UI.
|
||||
# return "sessions/atprotosocial/settings.html"
|
||||
"""Returns the path to the settings template for Blueski, if any."""
|
||||
# This template would be used to render Blueski-specific settings in the UI.
|
||||
# return "sessions/blueski/settings.html"
|
||||
return "sessions/generic/settings_auth_password.html" # If using simple handle/password auth
|
||||
|
||||
def get_user_action_templates(self) -> dict[str, str] | None:
|
||||
"""
|
||||
Returns a map of user action identifiers to their template paths for ATProtoSocial.
|
||||
Returns a map of user action identifiers to their template paths for Blueski.
|
||||
User actions are typically buttons or forms displayed on a user's profile.
|
||||
"""
|
||||
# TODO: Define templates for ATProtoSocial user actions
|
||||
# TODO: Define templates for Blueski user actions
|
||||
# Example:
|
||||
# return {
|
||||
# "view_profile_on_bsky": "sessions/atprotosocial/actions/view_profile_button.html",
|
||||
# "send_direct_message": "sessions/atprotosocial/actions/send_dm_form.html", # If DMs are supported
|
||||
# "view_profile_on_bsky": "sessions/blueski/actions/view_profile_button.html",
|
||||
# "send_direct_message": "sessions/blueski/actions/send_dm_form.html", # If DMs are supported
|
||||
# }
|
||||
return None # Placeholder
|
||||
|
||||
def get_user_list_action_templates(self) -> dict[str, str] | None:
|
||||
"""
|
||||
Returns a map of user list action identifiers to their template paths for ATProtoSocial.
|
||||
Returns a map of user list action identifiers to their template paths for Blueski.
|
||||
These actions might appear on lists of users (e.g., followers, following).
|
||||
"""
|
||||
# TODO: Define templates for ATProtoSocial user list actions
|
||||
# TODO: Define templates for Blueski user list actions
|
||||
# Example:
|
||||
# return {
|
||||
# "follow_all_visible": "sessions/atprotosocial/list_actions/follow_all_button.html",
|
||||
# "follow_all_visible": "sessions/blueski/list_actions/follow_all_button.html",
|
||||
# }
|
||||
return None # Placeholder
|
||||
|
||||
# Add any other template-related helper methods specific to ATProtoSocial.
|
||||
# Add any other template-related helper methods specific to Blueski.
|
||||
# For example, methods to get templates for specific types of content (images, polls)
|
||||
# if they need special rendering.
|
||||
|
||||
@@ -116,8 +116,8 @@ class ATProtoSocialTemplates:
|
||||
Returns a specific template path for a given message type (e.g., post, reply, quote).
|
||||
This can be useful if different types of messages need distinct rendering beyond the standard card.
|
||||
"""
|
||||
# TODO: Define specific templates if ATProtoSocial messages have varied structures
|
||||
# TODO: Define specific templates if Blueski messages have varied structures
|
||||
# that require different display logic.
|
||||
# if message_type == "quote_post":
|
||||
# return "sessions/atprotosocial/cards/quote_post.html"
|
||||
# return "sessions/blueski/cards/quote_post.html"
|
||||
return None # Default to standard message card if not specified
|
||||
@@ -16,7 +16,7 @@ fromapprove.notifications import NotificationError
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
fromapprove.sessions.atprotosocial.session import Session as ATProtoSocialSession
|
||||
fromapprove.sessions.blueski.session import Session as BlueskiSession
|
||||
# Define common type aliases if needed
|
||||
ATUserProfile = models.AppBskyActorDefs.ProfileViewDetailed
|
||||
ATPost = models.AppBskyFeedDefs.PostView
|
||||
@@ -27,8 +27,8 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ATProtoSocialUtils:
|
||||
def __init__(self, session: ATProtoSocialSession) -> None:
|
||||
class BlueskiUtils:
|
||||
def __init__(self, session: BlueskiSession) -> None:
|
||||
self.session = session
|
||||
# _own_did and _own_handle are now set by Session.login upon successful authentication
|
||||
# and directly on the util instance.
|
||||
@@ -47,7 +47,7 @@ class ATProtoSocialUtils:
|
||||
self._own_handle = self.session.client.me.handle
|
||||
return self.session.client
|
||||
|
||||
logger.warning("ATProtoSocialUtils: Client not available or not authenticated.")
|
||||
logger.warning("BlueskiUtils: Client not available or not authenticated.")
|
||||
# Optionally, try to trigger re-authentication if appropriate,
|
||||
# but generally, the caller should ensure session is ready.
|
||||
# For example, by calling session.start() or session.authorise()
|
||||
@@ -59,7 +59,7 @@ class ATProtoSocialUtils:
|
||||
"""Retrieves the authenticated user's profile information."""
|
||||
client = await self._get_client()
|
||||
if not client or not self.get_own_did(): # Use getter for _own_did
|
||||
logger.warning("ATProtoSocial client not available or user DID not known.")
|
||||
logger.warning("Blueski client not available or user DID not known.")
|
||||
return None
|
||||
try:
|
||||
# client.me should be populated after login by the SDK
|
||||
@@ -78,7 +78,7 @@ class ATProtoSocialUtils:
|
||||
return response
|
||||
return None
|
||||
except AtProtocolError as e:
|
||||
logger.error(f"Error fetching own ATProtoSocial profile: {e}")
|
||||
logger.error(f"Error fetching own Blueski profile: {e}")
|
||||
return None
|
||||
|
||||
def get_own_did(self) -> str | None:
|
||||
@@ -116,13 +116,13 @@ class ATProtoSocialUtils:
|
||||
**kwargs: Any
|
||||
) -> str | None: # Returns the ATURI of the new post, or None on failure
|
||||
"""
|
||||
Posts a status (skeet) to ATProtoSocial.
|
||||
Posts a status (skeet) to Blueski.
|
||||
Handles text, images, replies, quotes, and content warnings (labels).
|
||||
"""
|
||||
client = await self._get_client()
|
||||
if not client:
|
||||
logger.error("ATProtoSocial client not available for posting.")
|
||||
raise NotificationError(_("Not connected to ATProtoSocial. Please check your connection settings or log in."))
|
||||
logger.error("Blueski client not available for posting.")
|
||||
raise NotificationError(_("Not connected to Blueski. Please check your connection settings or log in."))
|
||||
|
||||
if not self.get_own_did():
|
||||
logger.error("Cannot post status: User DID not available.")
|
||||
@@ -130,7 +130,7 @@ class ATProtoSocialUtils:
|
||||
|
||||
try:
|
||||
# Prepare core post record
|
||||
post_record_data = {'text': text, 'created_at': client.get_current_time_iso()} # SDK handles datetime format
|
||||
post_record_data = {'text': text, 'createdAt': client.get_current_time_iso()} # SDK handles datetime format
|
||||
|
||||
if langs:
|
||||
post_record_data['langs'] = langs
|
||||
@@ -227,13 +227,13 @@ class ATProtoSocialUtils:
|
||||
record=final_post_record,
|
||||
)
|
||||
)
|
||||
logger.info(f"Successfully posted to ATProtoSocial. URI: {response.uri}")
|
||||
logger.info(f"Successfully posted to Blueski. URI: {response.uri}")
|
||||
return response.uri
|
||||
except AtProtocolError as e:
|
||||
logger.error(f"Error posting status to ATProtoSocial: {e.error} - {e.message}", exc_info=True)
|
||||
logger.error(f"Error posting status to Blueski: {e.error} - {e.message}", exc_info=True)
|
||||
raise NotificationError(_("Failed to post: {error} - {message}").format(error=e.error or "Error", message=e.message or "Protocol error")) from e
|
||||
except Exception as e: # Catch any other unexpected errors
|
||||
logger.error(f"Unexpected error posting status to ATProtoSocial: {e}", exc_info=True)
|
||||
logger.error(f"Unexpected error posting status to Blueski: {e}", exc_info=True)
|
||||
raise NotificationError(_("An unexpected error occurred while posting: {error}").format(error=str(e))) from e
|
||||
|
||||
|
||||
@@ -241,7 +241,7 @@ class ATProtoSocialUtils:
|
||||
"""Deletes a status (post) given its AT URI."""
|
||||
client = await self._get_client()
|
||||
if not client:
|
||||
logger.error("ATProtoSocial client not available for deleting post.")
|
||||
logger.error("Blueski client not available for deleting post.")
|
||||
return False
|
||||
if not self.get_own_did():
|
||||
logger.error("Cannot delete status: User DID not available.")
|
||||
@@ -268,10 +268,10 @@ class ATProtoSocialUtils:
|
||||
rkey=rkey,
|
||||
)
|
||||
)
|
||||
logger.info(f"Successfully deleted post {post_uri} from ATProtoSocial.")
|
||||
logger.info(f"Successfully deleted post {post_uri} from Blueski.")
|
||||
return True
|
||||
except AtProtocolError as e:
|
||||
logger.error(f"Error deleting post {post_uri} from ATProtoSocial: {e.error} - {e.message}")
|
||||
logger.error(f"Error deleting post {post_uri} from Blueski: {e.error} - {e.message}")
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error deleting post {post_uri}: {e}", exc_info=True)
|
||||
return False
|
||||
@@ -279,12 +279,12 @@ class ATProtoSocialUtils:
|
||||
|
||||
async def upload_media(self, file_path: str, mime_type: str, alt_text: str | None = None) -> dict[str, Any] | None:
|
||||
"""
|
||||
Uploads media (image) to ATProtoSocial.
|
||||
Uploads media (image) to Blueski.
|
||||
Returns a dictionary containing the SDK's BlobRef object and alt_text, or None on failure.
|
||||
"""
|
||||
client = await self._get_client()
|
||||
if not client:
|
||||
logger.error("ATProtoSocial client not available for media upload.")
|
||||
logger.error("Blueski client not available for media upload.")
|
||||
return None
|
||||
try:
|
||||
with open(file_path, "rb") as f:
|
||||
@@ -303,7 +303,7 @@ class ATProtoSocialUtils:
|
||||
logger.error(f"Media upload failed for {file_path}, no blob in response.")
|
||||
return None
|
||||
except AtProtocolError as e:
|
||||
logger.error(f"Error uploading media {file_path} to ATProtoSocial: {e.error} - {e.message}", exc_info=True)
|
||||
logger.error(f"Error uploading media {file_path} to Blueski: {e.error} - {e.message}", exc_info=True)
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error uploading media {file_path}: {e}", exc_info=True)
|
||||
return None
|
||||
@@ -341,7 +341,7 @@ class ATProtoSocialUtils:
|
||||
models.ComAtprotoRepoCreateRecord.Input(
|
||||
repo=self.get_own_did(),
|
||||
collection=ids.AppBskyGraphFollow, # "app.bsky.graph.follow"
|
||||
record=models.AppBskyGraphFollow.Main(subject=user_did, created_at=client.get_current_time_iso()),
|
||||
record=models.AppBskyGraphFollow.Main(subject=user_did, createdAt=client.get_current_time_iso()),
|
||||
)
|
||||
)
|
||||
logger.info(f"Successfully followed user {user_did}.")
|
||||
@@ -596,7 +596,7 @@ class ATProtoSocialUtils:
|
||||
models.ComAtprotoRepoCreateRecord.Input(
|
||||
repo=self.get_own_did(),
|
||||
collection=ids.AppBskyGraphBlock, # "app.bsky.graph.block"
|
||||
record=models.AppBskyGraphBlock.Main(subject=user_did, created_at=client.get_current_time_iso()),
|
||||
record=models.AppBskyGraphBlock.Main(subject=user_did, createdAt=client.get_current_time_iso()),
|
||||
)
|
||||
)
|
||||
logger.info(f"Successfully blocked user {user_did}. Block record URI: {response.uri}")
|
||||
@@ -1098,7 +1098,7 @@ class ATProtoSocialUtils:
|
||||
"""
|
||||
client = await self._get_client()
|
||||
if not client:
|
||||
logger.error("ATProtoSocial client not available for reporting.")
|
||||
logger.error("Blueski client not available for reporting.")
|
||||
return False
|
||||
|
||||
try:
|
||||
@@ -17,6 +17,11 @@ def compose_post(post, db, settings, relative_times, show_screen_names, safe=Tru
|
||||
text = _("Boosted from @{}: {}").format(post.reblog.account.acct, templates.process_text(post.reblog, safe=safe))
|
||||
else:
|
||||
text = templates.process_text(post, safe=safe)
|
||||
# Handle quoted posts
|
||||
if hasattr(post, 'quote') and post.quote != None and hasattr(post.quote, 'quoted_status') and post.quote.quoted_status != None:
|
||||
quoted_user = post.quote.quoted_status.account.acct
|
||||
quoted_text = templates.process_text(post.quote.quoted_status, safe=safe)
|
||||
text = text + " " + _("Quoting @{}: {}").format(quoted_user, quoted_text)
|
||||
filtered = utils.evaluate_filters(post=post, current_context="home")
|
||||
if filtered != None:
|
||||
text = _("hidden by filter {}").format(filtered)
|
||||
|
||||
@@ -248,6 +248,106 @@ class Session(base.baseSession):
|
||||
pub.sendMessage("mastodon.error_post", name=self.get_name(), reply_to=reply_to, visibility=visibility, posts=posts, lang=language)
|
||||
return
|
||||
|
||||
def edit_post(self, post_id, posts=[]):
|
||||
""" Convenience function to edit a post. Only the first item in posts list is used as threads cannot be edited.
|
||||
|
||||
Note: According to Mastodon API, not all fields can be edited. Visibility, language, and reply context cannot be changed.
|
||||
|
||||
Args:
|
||||
post_id: ID of the status to edit
|
||||
posts: List with post data. Only first item is used.
|
||||
|
||||
Returns:
|
||||
Updated status object or None on failure
|
||||
"""
|
||||
if len(posts) == 0:
|
||||
log.warning("edit_post called with empty posts list")
|
||||
return None
|
||||
|
||||
obj = posts[0]
|
||||
text = obj.get("text")
|
||||
|
||||
if not text:
|
||||
log.warning("edit_post called without text content")
|
||||
return None
|
||||
|
||||
media_ids = []
|
||||
media_attributes = []
|
||||
|
||||
try:
|
||||
poll = None
|
||||
# Handle poll attachments
|
||||
if len(obj["attachments"]) == 1 and obj["attachments"][0]["type"] == "poll":
|
||||
poll = self.api.make_poll(
|
||||
options=obj["attachments"][0]["options"],
|
||||
expires_in=obj["attachments"][0]["expires_in"],
|
||||
multiple=obj["attachments"][0]["multiple"],
|
||||
hide_totals=obj["attachments"][0]["hide_totals"]
|
||||
)
|
||||
log.debug("Editing post with poll (this will reset votes)")
|
||||
# Handle media attachments
|
||||
elif len(obj["attachments"]) > 0:
|
||||
for i in obj["attachments"]:
|
||||
# If attachment has an 'id', it's an existing media that we keep
|
||||
if "id" in i:
|
||||
media_ids.append(i["id"])
|
||||
# If existing media has metadata to update, use generate_media_edit_attributes
|
||||
if "description" in i or "focus" in i:
|
||||
media_attr = self.api.generate_media_edit_attributes(
|
||||
id=i["id"],
|
||||
description=i.get("description"),
|
||||
focus=i.get("focus")
|
||||
)
|
||||
media_attributes.append(media_attr)
|
||||
# Otherwise it's a new file to upload
|
||||
elif "file" in i:
|
||||
description = i.get("description", "")
|
||||
focus = i.get("focus", None)
|
||||
media = self.api_call(
|
||||
"media_post",
|
||||
media_file=i["file"],
|
||||
description=description,
|
||||
focus=focus,
|
||||
synchronous=True
|
||||
)
|
||||
media_ids.append(media.id)
|
||||
log.debug("Uploaded new media with id: {}".format(media.id))
|
||||
|
||||
# Prepare parameters for status_update
|
||||
update_params = {
|
||||
"id": post_id,
|
||||
"status": text,
|
||||
"_sound": "tweet_send.ogg",
|
||||
"sensitive": obj.get("sensitive", False),
|
||||
"spoiler_text": obj.get("spoiler_text", None),
|
||||
}
|
||||
|
||||
# Add optional parameters only if provided
|
||||
if media_ids:
|
||||
update_params["media_ids"] = media_ids
|
||||
if media_attributes:
|
||||
update_params["media_attributes"] = media_attributes
|
||||
if poll:
|
||||
update_params["poll"] = poll
|
||||
|
||||
# Call status_update API
|
||||
log.debug("Editing post {} with params: {}".format(post_id, {k: v for k, v in update_params.items() if k not in ["_sound"]}))
|
||||
item = self.api_call(call_name="status_update", **update_params)
|
||||
|
||||
if item:
|
||||
log.info("Successfully edited post {}".format(post_id))
|
||||
return item
|
||||
|
||||
except MastodonAPIError as e:
|
||||
log.exception("Mastodon API error updating post {}: {}".format(post_id, str(e)))
|
||||
output.speak(_("Error editing post: {}").format(str(e)))
|
||||
pub.sendMessage("mastodon.error_edit", name=self.get_name(), post_id=post_id, error=str(e))
|
||||
return None
|
||||
except Exception as e:
|
||||
log.exception("Unexpected error updating post {}: {}".format(post_id, str(e)))
|
||||
output.speak(_("Error editing post: {}").format(str(e)))
|
||||
return None
|
||||
|
||||
def get_name(self):
|
||||
instance = self.settings["mastodon"]["instance"]
|
||||
instance = instance.replace("https://", "")
|
||||
|
||||
@@ -76,6 +76,13 @@ def render_post(post, template, settings, relative_times=False, offset_hours=0):
|
||||
else:
|
||||
text = process_text(post, safe=False)
|
||||
safe_text = process_text(post)
|
||||
# Handle quoted posts
|
||||
if hasattr(post, 'quote') and post.quote != None and hasattr(post.quote, 'quoted_status') and post.quote.quoted_status != None:
|
||||
quoted_user = post.quote.quoted_status.account.acct
|
||||
quoted_text = process_text(post.quote.quoted_status, safe=False)
|
||||
quoted_safe_text = process_text(post.quote.quoted_status, safe=True)
|
||||
text = text + " " + _("Quoting @{}: {}").format(quoted_user, quoted_text)
|
||||
safe_text = safe_text + " " + _("Quoting @{}: {}").format(quoted_user, quoted_safe_text)
|
||||
filtered = utils.evaluate_filters(post=post, current_context="home")
|
||||
if filtered != None:
|
||||
text = _("hidden by filter {}").format(filtered)
|
||||
|
||||
@@ -3,23 +3,47 @@ import demoji
|
||||
from html.parser import HTMLParser
|
||||
from datetime import datetime, timezone
|
||||
|
||||
url_re = re.compile('<a\s*href=[\'|"](.*?)[\'"].*?>')
|
||||
url_re = re.compile(r'<a\s*href=[\'|"](.*?)[\'"].*?>')
|
||||
|
||||
class HTMLFilter(HTMLParser):
|
||||
# Classes to ignore when parsing HTML
|
||||
IGNORED_CLASSES = ["quote-inline"]
|
||||
|
||||
text = ""
|
||||
first_paragraph = True
|
||||
skip_depth = 0 # Track nesting depth of ignored elements
|
||||
|
||||
def handle_data(self, data):
|
||||
self.text += data
|
||||
# Only add data if we're not inside an ignored element
|
||||
if self.skip_depth == 0:
|
||||
self.text += data
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag == "br":
|
||||
self.text = self.text+"\n"
|
||||
elif tag == "p":
|
||||
if self.first_paragraph:
|
||||
self.first_paragraph = False
|
||||
else:
|
||||
self.text = self.text+"\n\n"
|
||||
# Check if this tag has a class that should be ignored
|
||||
attrs_dict = dict(attrs)
|
||||
tag_class = attrs_dict.get("class", "")
|
||||
|
||||
# Check if any ignored class is present in this tag
|
||||
should_skip = any(ignored_class in tag_class for ignored_class in self.IGNORED_CLASSES)
|
||||
|
||||
if should_skip:
|
||||
self.skip_depth += 1
|
||||
elif self.skip_depth == 0: # Only process tags if we're not skipping
|
||||
if tag == "br":
|
||||
self.text = self.text+"\n"
|
||||
elif tag == "p":
|
||||
if self.first_paragraph:
|
||||
self.first_paragraph = False
|
||||
else:
|
||||
self.text = self.text+"\n\n"
|
||||
else:
|
||||
# We're inside a skipped element, increment depth for nested tags
|
||||
self.skip_depth += 1
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
# Decrement skip depth when closing any tag while skipping
|
||||
if self.skip_depth > 0:
|
||||
self.skip_depth -= 1
|
||||
|
||||
def html_filter(data):
|
||||
f = HTMLFilter()
|
||||
|
||||
@@ -4,7 +4,7 @@ import unittest
|
||||
from unittest.mock import patch, AsyncMock, MagicMock, PropertyMock
|
||||
|
||||
# Assuming paths are set up correctly for test environment to find these
|
||||
from sessions.atprotosocial.session import Session as ATProtoSocialSession
|
||||
from sessions.blueski.session import Session as BlueskiSession
|
||||
from sessions.session_exceptions import SessionLoginError, SessionError
|
||||
from approve.notifications import NotificationError # Assuming this is the correct import path
|
||||
from atproto.xrpc_client.models.common import XrpcError
|
||||
@@ -48,7 +48,7 @@ mock_wx.ICON_QUESTION = 32 # Example
|
||||
|
||||
# Mock config objects
|
||||
# This structure tries to mimic how config is accessed in session.py
|
||||
# e.g., config.sessions.atprotosocial[user_id].handle
|
||||
# e.g., config.sessions.blueski[user_id].handle
|
||||
class MockConfigNode:
|
||||
def __init__(self, initial_value=None):
|
||||
self._value = initial_value
|
||||
@@ -60,9 +60,9 @@ class MockUserSessionConfig:
|
||||
self.handle = MockConfigNode("")
|
||||
self.app_password = MockConfigNode("")
|
||||
self.did = MockConfigNode("")
|
||||
# Add other config values if session.py uses them for atprotosocial
|
||||
# Add other config values if session.py uses them for blueski
|
||||
|
||||
class MockATProtoSocialConfig:
|
||||
class MockBlueskiConfig:
|
||||
def __init__(self):
|
||||
self._user_configs = {"test_user": MockUserSessionConfig()}
|
||||
def __getitem__(self, key):
|
||||
@@ -70,31 +70,31 @@ class MockATProtoSocialConfig:
|
||||
|
||||
class MockSessionsConfig:
|
||||
def __init__(self):
|
||||
self.atprotosocial = MockATProtoSocialConfig()
|
||||
self.blueski = MockBlueskiConfig()
|
||||
|
||||
mock_config_global = MagicMock()
|
||||
mock_config_global.sessions = MockSessionsConfig()
|
||||
|
||||
|
||||
class TestATProtoSocialSession(unittest.IsolatedAsyncioTestCase):
|
||||
class TestBlueskiSession(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
@patch('sessions.atprotosocial.session.wx', mock_wx)
|
||||
@patch('sessions.atprotosocial.session.config', mock_config_global)
|
||||
@patch('sessions.blueski.session.wx', mock_wx)
|
||||
@patch('sessions.blueski.session.config', mock_config_global)
|
||||
def setUp(self):
|
||||
self.mock_approval_api = MagicMock()
|
||||
|
||||
# Reset mocks for user_config part of global mock_config_global for each test
|
||||
self.mock_user_config_instance = MockUserSessionConfig()
|
||||
mock_config_global.sessions.atprotosocial.__getitem__.return_value = self.mock_user_config_instance
|
||||
mock_config_global.sessions.blueski.__getitem__.return_value = self.mock_user_config_instance
|
||||
|
||||
self.session = ATProtoSocialSession(approval_api=self.mock_approval_api, user_id="test_user", channel_id="test_channel")
|
||||
self.session = BlueskiSession(approval_api=self.mock_approval_api, user_id="test_user", channel_id="test_channel")
|
||||
|
||||
self.session.db = {}
|
||||
self.session.save_db = AsyncMock()
|
||||
self.session.notify_session_ready = AsyncMock()
|
||||
self.session.send_text_notification = MagicMock()
|
||||
|
||||
# Mock the util property to return a MagicMock for ATProtoSocialUtils
|
||||
# Mock the util property to return a MagicMock for BlueskiUtils
|
||||
self.mock_util_instance = AsyncMock() # Make it an AsyncMock if its methods are async
|
||||
self.mock_util_instance._own_did = None # These are set directly by session.login
|
||||
self.mock_util_instance._own_handle = None
|
||||
@@ -104,12 +104,12 @@ class TestATProtoSocialSession(unittest.IsolatedAsyncioTestCase):
|
||||
self.session.util # Call property to ensure _util is set if it's lazy loaded
|
||||
|
||||
def test_session_initialization(self):
|
||||
self.assertIsInstance(self.session, ATProtoSocialSession)
|
||||
self.assertEqual(self.session.KIND, "atprotosocial")
|
||||
self.assertIsInstance(self.session, BlueskiSession)
|
||||
self.assertEqual(self.session.KIND, "blueski")
|
||||
self.assertIsNone(self.session.client)
|
||||
self.assertEqual(self.session.user_id, "test_user")
|
||||
|
||||
@patch('sessions.atprotosocial.session.AsyncClient')
|
||||
@patch('sessions.blueski.session.AsyncClient')
|
||||
async def test_login_successful(self, MockAsyncClient):
|
||||
mock_client_instance = MockAsyncClient.return_value
|
||||
# Use actual ATProto models for spec if possible for better type checking in mocks
|
||||
@@ -142,7 +142,7 @@ class TestATProtoSocialSession(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
self.session.notify_session_ready.assert_called_once()
|
||||
|
||||
@patch('sessions.atprotosocial.session.AsyncClient')
|
||||
@patch('sessions.blueski.session.AsyncClient')
|
||||
async def test_login_failure_xrpc(self, MockAsyncClient):
|
||||
mock_client_instance = MockAsyncClient.return_value
|
||||
mock_client_instance.login = AsyncMock(side_effect=XrpcError(error="AuthenticationFailed", message="Invalid credentials"))
|
||||
@@ -155,8 +155,8 @@ class TestATProtoSocialSession(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertIsNone(self.session.client)
|
||||
self.session.notify_session_ready.assert_not_called()
|
||||
|
||||
@patch('sessions.atprotosocial.session.wx', new=mock_wx)
|
||||
@patch.object(ATProtoSocialSession, 'login', new_callable=AsyncMock)
|
||||
@patch('sessions.blueski.session.wx', new=mock_wx)
|
||||
@patch.object(BlueskiSession, 'login', new_callable=AsyncMock)
|
||||
async def test_authorise_successful(self, mock_login_method):
|
||||
mock_login_method.return_value = True
|
||||
|
||||
@@ -174,8 +174,8 @@ class TestATProtoSocialSession(unittest.IsolatedAsyncioTestCase):
|
||||
# Further check if wx.MessageBox was called with success
|
||||
# This requires more complex mocking or inspection of calls to mock_wx.MessageBox
|
||||
|
||||
@patch('sessions.atprotosocial.session.wx', new=mock_wx)
|
||||
@patch.object(ATProtoSocialSession, 'login', new_callable=AsyncMock)
|
||||
@patch('sessions.blueski.session.wx', new=mock_wx)
|
||||
@patch.object(BlueskiSession, 'login', new_callable=AsyncMock)
|
||||
async def test_authorise_login_fails_with_notification_error(self, mock_login_method):
|
||||
mock_login_method.side_effect = NotificationError("Specific login failure from mock.")
|
||||
|
||||
@@ -220,7 +220,7 @@ class TestATProtoSocialSession(unittest.IsolatedAsyncioTestCase):
|
||||
cw_text=None, is_sensitive=False, langs=["en", "es"], tags=None
|
||||
)
|
||||
|
||||
@patch('sessions.atprotosocial.session.os.path.basename', return_value="image.png") # Mock os.path.basename
|
||||
@patch('sessions.blueski.session.os.path.basename', return_value="image.png") # Mock os.path.basename
|
||||
async def test_send_post_with_media(self, mock_basename):
|
||||
self.session.is_ready = MagicMock(return_value=True)
|
||||
mock_blob_info = {"blob_ref": MagicMock(spec=atp_models.ComAtprotoRepoStrongRef.Blob), "alt_text": "A test image"}
|
||||
@@ -360,4 +360,3 @@ if 'wx' not in sys.modules: # type: ignore
|
||||
mock_wx_module.MessageBox = MockWxMessageBox
|
||||
mock_wx_module.CallAfter = MagicMock()
|
||||
mock_wx_module.GetApp = MagicMock()
|
||||
>>>>>>> REPLACE
|
||||
@@ -11,10 +11,10 @@ from datetime import datetime
|
||||
|
||||
from multiplatform_widgets import widgets
|
||||
|
||||
log = logging.getLogger("wxUI.buffers.atprotosocial.panels")
|
||||
log = logging.getLogger("wxUI.buffers.blueski.panels")
|
||||
|
||||
|
||||
class ATProtoSocialHomeTimelinePanel(object):
|
||||
class BlueskiHomeTimelinePanel(object):
|
||||
"""Minimal Home timeline buffer for Bluesky.
|
||||
|
||||
Exposes a .buffer wx.Panel with a List control and provides
|
||||
@@ -27,6 +27,7 @@ class ATProtoSocialHomeTimelinePanel(object):
|
||||
self.account = session.get_name()
|
||||
self.name = name
|
||||
self.type = "home_timeline"
|
||||
self.timeline_algorithm = None
|
||||
self.invisible = True
|
||||
self.needs_init = True
|
||||
self.buffer = _HomePanel(parent, name)
|
||||
@@ -49,17 +50,16 @@ class ATProtoSocialHomeTimelinePanel(object):
|
||||
# The atproto SDK expects params, not raw kwargs
|
||||
try:
|
||||
from atproto import models as at_models # type: ignore
|
||||
# Home: algorithmic/default timeline
|
||||
try:
|
||||
params = at_models.AppBskyFeedGetTimeline.Params(limit=count)
|
||||
res = api.app.bsky.feed.get_timeline(params)
|
||||
except Exception:
|
||||
# Some SDKs may require explicit algorithm for home; try behavioral
|
||||
params = at_models.AppBskyFeedGetTimeline.Params(limit=count, algorithm="behavioral")
|
||||
res = api.app.bsky.feed.get_timeline(params)
|
||||
params = at_models.AppBskyFeedGetTimeline.Params(
|
||||
limit=count,
|
||||
algorithm=self.timeline_algorithm
|
||||
)
|
||||
res = api.app.bsky.feed.get_timeline(params)
|
||||
except Exception:
|
||||
# Fallback to plain dict params if typed models unavailable
|
||||
res = api.app.bsky.feed.get_timeline({"limit": count})
|
||||
payload = {"limit": count}
|
||||
if self.timeline_algorithm:
|
||||
payload["algorithm"] = self.timeline_algorithm
|
||||
res = api.app.bsky.feed.get_timeline(payload)
|
||||
feed = getattr(res, "feed", [])
|
||||
self.cursor = getattr(res, "cursor", None)
|
||||
self.items = []
|
||||
@@ -103,10 +103,17 @@ class ATProtoSocialHomeTimelinePanel(object):
|
||||
api = self.session._ensure_client()
|
||||
try:
|
||||
from atproto import models as at_models # type: ignore
|
||||
params = at_models.AppBskyFeedGetTimeline.Params(limit=40, cursor=self.cursor)
|
||||
params = at_models.AppBskyFeedGetTimeline.Params(
|
||||
limit=40,
|
||||
cursor=self.cursor,
|
||||
algorithm=self.timeline_algorithm
|
||||
)
|
||||
res = api.app.bsky.feed.get_timeline(params)
|
||||
except Exception:
|
||||
res = api.app.bsky.feed.get_timeline({"limit": 40, "cursor": self.cursor})
|
||||
payload = {"limit": 40, "cursor": self.cursor}
|
||||
if self.timeline_algorithm:
|
||||
payload["algorithm"] = self.timeline_algorithm
|
||||
res = api.app.bsky.feed.get_timeline(payload)
|
||||
feed = getattr(res, "feed", [])
|
||||
self.cursor = getattr(res, "cursor", None)
|
||||
new_items = []
|
||||
@@ -144,7 +151,7 @@ class ATProtoSocialHomeTimelinePanel(object):
|
||||
log.exception("Failed to load more Bluesky timeline items")
|
||||
return 0
|
||||
|
||||
# Alias to integrate with mainController expectations for ATProto
|
||||
# Alias to integrate with mainController expectations for Blueski
|
||||
def load_more_posts(self, *args, **kwargs):
|
||||
return self.get_more_items()
|
||||
|
||||
@@ -281,12 +288,13 @@ class _HomePanel(wx.Panel):
|
||||
self.SetSizer(sizer)
|
||||
|
||||
|
||||
class ATProtoSocialFollowingTimelinePanel(ATProtoSocialHomeTimelinePanel):
|
||||
class BlueskiFollowingTimelinePanel(BlueskiHomeTimelinePanel):
|
||||
"""Following-only timeline (reverse-chronological)."""
|
||||
|
||||
def __init__(self, parent, name: str, session):
|
||||
super().__init__(parent, name, session)
|
||||
self.type = "following_timeline"
|
||||
self.timeline_algorithm = "reverse-chronological"
|
||||
# Make sure the underlying wx panel also reflects this type
|
||||
try:
|
||||
self.buffer.type = "following_timeline"
|
||||
@@ -302,7 +310,7 @@ class ATProtoSocialFollowingTimelinePanel(ATProtoSocialHomeTimelinePanel):
|
||||
api = self.session._ensure_client()
|
||||
# Following timeline via reverse-chronological algorithm on get_timeline
|
||||
# Use plain dict to avoid typed-model mismatches across SDK versions
|
||||
res = api.app.bsky.feed.get_timeline({"limit": count, "algorithm": "reverse-chronological"})
|
||||
res = api.app.bsky.feed.get_timeline({"limit": count, "algorithm": self.timeline_algorithm})
|
||||
feed = getattr(res, "feed", [])
|
||||
self.cursor = getattr(res, "cursor", None)
|
||||
self.items = []
|
||||
@@ -343,7 +351,11 @@ class ATProtoSocialFollowingTimelinePanel(ATProtoSocialHomeTimelinePanel):
|
||||
try:
|
||||
api = self.session._ensure_client()
|
||||
# Pagination via reverse-chronological algorithm on get_timeline
|
||||
res = api.app.bsky.feed.get_timeline({"limit": 40, "cursor": self.cursor, "algorithm": "reverse-chronological"})
|
||||
res = api.app.bsky.feed.get_timeline({
|
||||
"limit": 40,
|
||||
"cursor": self.cursor,
|
||||
"algorithm": self.timeline_algorithm
|
||||
})
|
||||
feed = getattr(res, "feed", [])
|
||||
self.cursor = getattr(res, "cursor", None)
|
||||
new_items = []
|
||||
@@ -6,9 +6,9 @@ from pubsub import pub
|
||||
|
||||
from approve.translation import translate as _
|
||||
from approve.notifications import NotificationError
|
||||
# Assuming controller.atprotosocial.userList.get_user_profile_details and session.util._format_profile_data exist
|
||||
# Assuming controller.blueski.userList.get_user_profile_details and session.util._format_profile_data exist
|
||||
# For direct call to util:
|
||||
# from sessions.atprotosocial import utils as ATProtoSocialUtils
|
||||
# from sessions.blueski import utils as BlueskiUtils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -272,7 +272,7 @@ class ShowUserProfileDialog(wx.Dialog):
|
||||
self.SetTitle(f"{_('User Profile')} - {text}")
|
||||
|
||||
```python
|
||||
# Example of how this dialog might be called from atprotosocial.Handler.user_details:
|
||||
# Example of how this dialog might be called from blueski.Handler.user_details:
|
||||
# (This is conceptual, actual integration in handler.py will use the dialog)
|
||||
#
|
||||
# async def user_details(self, buffer_panel_or_user_ident):
|
||||
@@ -8,6 +8,8 @@ class base(wx.Menu):
|
||||
self.Append(self.boost)
|
||||
self.reply = wx.MenuItem(self, wx.ID_ANY, _(u"Re&ply"))
|
||||
self.Append(self.reply)
|
||||
self.edit = wx.MenuItem(self, wx.ID_ANY, _(u"&Edit"))
|
||||
self.Append(self.edit)
|
||||
self.fav = wx.MenuItem(self, wx.ID_ANY, _(u"&Add to favorites"))
|
||||
self.Append(self.fav)
|
||||
self.unfav = wx.MenuItem(self, wx.ID_ANY, _(u"R&emove from favorites"))
|
||||
@@ -36,6 +38,8 @@ class notification(wx.Menu):
|
||||
self.Append(self.boost)
|
||||
self.reply = wx.MenuItem(self, wx.ID_ANY, _(u"Re&ply"))
|
||||
self.Append(self.reply)
|
||||
self.edit = wx.MenuItem(self, wx.ID_ANY, _(u"&Edit"))
|
||||
self.Append(self.edit)
|
||||
self.fav = wx.MenuItem(self, wx.ID_ANY, _(u"&Add to favorites"))
|
||||
self.Append(self.fav)
|
||||
self.unfav = wx.MenuItem(self, wx.ID_ANY, _(u"R&emove from favorites"))
|
||||
|
||||
@@ -51,7 +51,7 @@ class Post(wx.Dialog):
|
||||
visibility_sizer.Add(self.visibility, 0, 0, 0)
|
||||
language_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
post_actions_sizer.Add(language_sizer, 0, wx.RIGHT, 20)
|
||||
lang_label = wx.StaticText(self, wx.ID_ANY, _("Language"))
|
||||
lang_label = wx.StaticText(self, wx.ID_ANY, _("&Language"))
|
||||
language_sizer.Add(lang_label, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
|
||||
self.language = wx.ComboBox(self, wx.ID_ANY, choices=languages, style=wx.CB_DROPDOWN | wx.CB_READONLY)
|
||||
language_sizer.Add(self.language, 0, wx.ALIGN_CENTER_VERTICAL, 0)
|
||||
@@ -234,9 +234,9 @@ class viewPost(wx.Dialog):
|
||||
|
||||
def create_buttons_section(self, panel):
|
||||
sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
self.mute = wx.Button(panel, wx.ID_ANY, _("Mute conversation"))
|
||||
self.mute = wx.Button(panel, wx.ID_ANY, _("&Mute conversation"))
|
||||
self.mute.Enable(False)
|
||||
self.share = wx.Button(panel, wx.ID_ANY, _("Copy link to clipboard"))
|
||||
self.share = wx.Button(panel, wx.ID_ANY, _("&Copy link to clipboard"))
|
||||
self.share.Enable(False)
|
||||
self.spellcheck = wx.Button(panel, wx.ID_ANY, _("Check &spelling..."))
|
||||
self.translateButton = wx.Button(panel, wx.ID_ANY, _("&Translate..."))
|
||||
@@ -295,7 +295,7 @@ class poll(wx.Dialog):
|
||||
sizer_1 = wx.BoxSizer(wx.VERTICAL)
|
||||
period_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
sizer_1.Add(period_sizer, 1, wx.EXPAND, 0)
|
||||
label_period = wx.StaticText(self, wx.ID_ANY, _("Participation time"))
|
||||
label_period = wx.StaticText(self, wx.ID_ANY, _("&Participation time"))
|
||||
period_sizer.Add(label_period, 0, 0, 0)
|
||||
self.period = wx.ComboBox(self, wx.ID_ANY, choices=[_("5 minutes"), _("30 minutes"), _("1 hour"), _("6 hours"), _("1 day"), _("2 days"), _("3 days"), _("4 days"), _("5 days"), _("6 days"), _("7 days")], style=wx.CB_DROPDOWN | wx.CB_READONLY | wx.CB_SIMPLE)
|
||||
self.period.SetFocus()
|
||||
@@ -305,36 +305,36 @@ class poll(wx.Dialog):
|
||||
sizer_1.Add(sizer_2, 1, wx.EXPAND, 0)
|
||||
option1_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
sizer_2.Add(option1_sizer, 1, wx.EXPAND, 0)
|
||||
label_2 = wx.StaticText(self, wx.ID_ANY, _("Option 1"))
|
||||
label_2 = wx.StaticText(self, wx.ID_ANY, _("Option &1"))
|
||||
option1_sizer.Add(label_2, 0, 0, 0)
|
||||
self.option1 = wx.TextCtrl(self, wx.ID_ANY, "")
|
||||
self.option1.SetMaxLength(25)
|
||||
option1_sizer.Add(self.option1, 0, 0, 0)
|
||||
option2_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
sizer_2.Add(option2_sizer, 1, wx.EXPAND, 0)
|
||||
label_3 = wx.StaticText(self, wx.ID_ANY, _("Option 2"))
|
||||
label_3 = wx.StaticText(self, wx.ID_ANY, _("Option &2"))
|
||||
option2_sizer.Add(label_3, 0, 0, 0)
|
||||
self.option2 = wx.TextCtrl(self, wx.ID_ANY, "")
|
||||
self.option2.SetMaxLength(25)
|
||||
option2_sizer.Add(self.option2, 0, 0, 0)
|
||||
option3_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
sizer_2.Add(option3_sizer, 1, wx.EXPAND, 0)
|
||||
label_4 = wx.StaticText(self, wx.ID_ANY, _("Option 3"))
|
||||
label_4 = wx.StaticText(self, wx.ID_ANY, _("Option &3"))
|
||||
option3_sizer.Add(label_4, 0, 0, 0)
|
||||
self.option3 = wx.TextCtrl(self, wx.ID_ANY, "")
|
||||
self.option3.SetMaxLength(25)
|
||||
option3_sizer.Add(self.option3, 0, 0, 0)
|
||||
option4_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
sizer_2.Add(option4_sizer, 1, wx.EXPAND, 0)
|
||||
label_5 = wx.StaticText(self, wx.ID_ANY, _("Option 4"))
|
||||
label_5 = wx.StaticText(self, wx.ID_ANY, _("Option &4"))
|
||||
option4_sizer.Add(label_5, 0, 0, 0)
|
||||
self.option4 = wx.TextCtrl(self, wx.ID_ANY, "")
|
||||
self.option4.SetMaxLength(25)
|
||||
option4_sizer.Add(self.option4, 0, 0, 0)
|
||||
self.multiple = wx.CheckBox(self, wx.ID_ANY, _("Allow multiple choices per user"))
|
||||
self.multiple = wx.CheckBox(self, wx.ID_ANY, _("&Allow multiple choices per user"))
|
||||
self.multiple.SetValue(False)
|
||||
sizer_1.Add(self.multiple, 0, wx.ALL, 5)
|
||||
self.hide_votes = wx.CheckBox(self, wx.ID_ANY, _("Hide votes count until the poll expires"))
|
||||
self.hide_votes = wx.CheckBox(self, wx.ID_ANY, _("&Hide votes count until the poll expires"))
|
||||
self.hide_votes.SetValue(False)
|
||||
sizer_1.Add(self.hide_votes, 0, wx.ALL, 5)
|
||||
btn_sizer = wx.StdDialogButtonSizer()
|
||||
|
||||
@@ -141,7 +141,7 @@ class ShowUserProfile(wx.Dialog):
|
||||
mainSizer.Add(privateSizer, 0, wx.ALL | wx.CENTER)
|
||||
|
||||
botSizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
botLabel = wx.StaticText(self.panel, label=_("&Bot account: "))
|
||||
botLabel = wx.StaticText(self.panel, label=_("B&ot account: "))
|
||||
botText = self.createTextCtrl(bullSwitch[user.bot], (30, 30))
|
||||
botSizer.Add(botLabel, wx.SizerFlags().Center())
|
||||
botSizer.Add(botText, wx.SizerFlags().Center())
|
||||
@@ -154,7 +154,7 @@ class ShowUserProfile(wx.Dialog):
|
||||
discoverSizer.Add(discoverText, wx.SizerFlags().Center())
|
||||
mainSizer.Add(discoverSizer, 0, wx.ALL | wx.CENTER)
|
||||
|
||||
posts = wx.Button(self.panel, label=_("{} p&osts. Click to open posts timeline").format(user.statuses_count))
|
||||
posts = wx.Button(self.panel, label=_("{} pos&ts. Click to open posts timeline").format(user.statuses_count))
|
||||
# posts.SetToolTip(_("Click to open {}'s posts").format(user.display_name))
|
||||
posts.Bind(wx.EVT_BUTTON, self.onPost)
|
||||
mainSizer.Add(posts, wx.SizerFlags().Center())
|
||||
|
||||
@@ -119,7 +119,7 @@ class UpdateProfileDialog(wx.Dialog):
|
||||
|
||||
self.locked = wx.CheckBox(panel, label=_("&Private account"))
|
||||
self.locked.SetValue(locked)
|
||||
self.bot = wx.CheckBox(panel, label=_("&Bot account"))
|
||||
self.bot = wx.CheckBox(panel, label=_("B&ot account"))
|
||||
self.bot.SetValue(bot)
|
||||
self.discoverable = wx.CheckBox(panel, label=_("&Discoverable account"))
|
||||
self.discoverable.SetValue(discoverable)
|
||||
|
||||
@@ -26,7 +26,7 @@ class EditTemplateDialog(wx.Dialog):
|
||||
sizer_3.AddButton(self.button_SAVE)
|
||||
self.button_CANCEL = wx.Button(self, wx.ID_CANCEL)
|
||||
sizer_3.AddButton(self.button_CANCEL)
|
||||
self.button_RESTORE = wx.Button(self, wx.ID_ANY, _("Restore template"))
|
||||
self.button_RESTORE = wx.Button(self, wx.ID_ANY, _("&Restore template"))
|
||||
self.button_RESTORE.Bind(wx.EVT_BUTTON, self.on_restore)
|
||||
sizer_3.AddButton(self.button_CANCEL)
|
||||
sizer_3.Realize()
|
||||
|
||||
@@ -22,11 +22,11 @@ class UserListDialog(wx.Dialog):
|
||||
user_list_sizer.Add(self.user_list, 1, wx.EXPAND | wx.ALL, 10)
|
||||
main_sizer.Add(user_list_sizer, 1, wx.EXPAND | wx.ALL, 15)
|
||||
buttons_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
self.actions_button = wx.Button(panel, wx.ID_ANY, "Actions")
|
||||
self.actions_button = wx.Button(panel, wx.ID_ANY, "&Actions")
|
||||
buttons_sizer.Add(self.actions_button, 0, wx.RIGHT, 10)
|
||||
self.details_button = wx.Button(panel, wx.ID_ANY, _("View profile"))
|
||||
self.details_button = wx.Button(panel, wx.ID_ANY, _("&View profile"))
|
||||
buttons_sizer.Add(self.details_button, 0, wx.RIGHT, 10)
|
||||
close_button = wx.Button(panel, wx.ID_CANCEL, "Close")
|
||||
close_button = wx.Button(panel, wx.ID_CANCEL, "&Close")
|
||||
buttons_sizer.Add(close_button, 0)
|
||||
main_sizer.Add(buttons_sizer, 0, wx.ALIGN_CENTER | wx.BOTTOM, 15)
|
||||
panel.SetSizer(main_sizer)
|
||||
|
||||
@@ -19,7 +19,7 @@ class mainFrame(wx.Frame):
|
||||
self.menuitem_search = self.menubar_application.Append(wx.ID_ANY, _(u"&Search"))
|
||||
self.lists = self.menubar_application.Append(wx.ID_ANY, _(u"&Lists manager"))
|
||||
self.lists.Enable(False)
|
||||
self.manageAliases = self.menubar_application.Append(wx.ID_ANY, _("Manage user aliases"))
|
||||
self.manageAliases = self.menubar_application.Append(wx.ID_ANY, _("M&anage user aliases"))
|
||||
self.keystroke_editor = self.menubar_application.Append(wx.ID_ANY, _(u"&Edit keystrokes"))
|
||||
self.account_settings = self.menubar_application.Append(wx.ID_ANY, _(u"Account se&ttings"))
|
||||
self.prefs = self.menubar_application.Append(wx.ID_PREFERENCES, _(u"&Global settings"))
|
||||
@@ -56,7 +56,7 @@ class mainFrame(wx.Frame):
|
||||
self.trends = self.menubar_buffer.Append(wx.ID_ANY, _(u"New &trending topics buffer..."))
|
||||
self.filter = self.menubar_buffer.Append(wx.ID_ANY, _(u"Create a &filter"))
|
||||
self.manage_filters = self.menubar_buffer.Append(wx.ID_ANY, _(u"&Manage filters"))
|
||||
self.find = self.menubar_buffer.Append(wx.ID_ANY, _(u"Find a string in the currently focused buffer..."))
|
||||
self.find = self.menubar_buffer.Append(wx.ID_ANY, _(u"F&ind a string in the currently focused buffer..."))
|
||||
self.load_previous_items = self.menubar_buffer.Append(wx.ID_ANY, _(u"&Load previous items"))
|
||||
self.menubar_buffer.AppendSeparator()
|
||||
self.mute_buffer = self.menubar_buffer.AppendCheckItem(wx.ID_ANY, _(u"&Mute"))
|
||||
@@ -66,8 +66,8 @@ class mainFrame(wx.Frame):
|
||||
|
||||
# audio menu
|
||||
self.menubar_audio = wx.Menu()
|
||||
self.seekLeft = self.menubar_audio.Append(wx.ID_ANY, _(u"&Seek back 5 seconds"))
|
||||
self.seekRight = self.menubar_audio.Append(wx.ID_ANY, _(u"&Seek forward 5 seconds"))
|
||||
self.seekLeft = self.menubar_audio.Append(wx.ID_ANY, _(u"Seek &back 5 seconds"))
|
||||
self.seekRight = self.menubar_audio.Append(wx.ID_ANY, _(u"Seek &forward 5 seconds"))
|
||||
|
||||
# Help Menu
|
||||
self.menubar_help = wx.Menu()
|
||||
|
||||
Reference in New Issue
Block a user