Files
twblue/src/sessions/atprotosocial/compose.py

154 lines
8.2 KiB
Python
Raw Normal View History

feat: Initial integration of ATProtoSocial (Bluesky) protocol This commit introduces the initial implementation for supporting the ATProtoSocial (Bluesky) protocol within your application. Key changes and features I implemented: 1. **Core Protocol Structure:** * I added new directories `src/sessions/atprotosocial` and `src/controller/atprotosocial`. * I populated these with foundational files (`session.py`, `utils.py`, `handler.py`, `compose.py`, etc.), mirroring the Mastodon implementation structure but adapted for ATProtoSocial. 2. **Authentication:** * I implemented login and authorization using Bluesky SDK (handle and app password) in `sessions/atprotosocial/session.py`. * I integrated this into your session management UI (`sessionManagerDialog.py`) to allow adding ATProtoSocial accounts. 3. **Posting Capabilities:** * I implemented sending text posts, posts with images, replies, and quoting posts in `sessions/atprotosocial/session.py` and `utils.py`. * I updated `compose.py` to reflect ATProtoSocial's panel configuration (character limits, media support, quoting). 4. **Notifications:** * I implemented fetching and processing of notifications (likes, reposts, follows, mentions, replies, quotes) in `sessions/atprotosocial/session.py`. * Notifications are formatted for display. 5. **Timelines:** * I implemented fetching and processing for home timeline and user-specific timelines in `sessions/atprotosocial/session.py`. * This includes handling of posts, reposts, and replies within your application's buffer and message cache system. 6. **User Actions:** * I implemented core user actions: follow, unfollow, mute, unmute, block, unblock in `sessions/atprotosocial/utils.py`. * I integrated these actions into the controller layer (`controller/atprotosocial/handler.py`) and exposed them via `session.get_user_actions()`. 7. **User Management & Profile:** * I implemented fetching user profiles, follower lists, following lists, and user search in `sessions/atprotosocial/utils.py` and `controller/atprotosocial/userList.py`. 8. **UI Integration (Initial Pass):** * I adapted your session management UI for ATProtoSocial account creation. * I updated main controller logic to load the ATProtoSocial handler and create basic buffers (Home, Notifications). * I modified menu item labels based on the active session type (e.g., "Post" vs "Toot", "Like" vs "Favorite"). * I integrated core actions like reposting and liking into existing UI flows. * I added basic integration for timeline refresh and loading more items. * I added placeholder integration for viewing user profiles and user-specific timelines. **Current Status & Next Steps:** This represents a significant portion of the ATProtoSocial integration. The backend logic for most core features is in place. The immediate next steps, which were part of the original plan but not yet completed, would be: * **Refining UI elements:** Fully implementing dedicated dialogs (compose, user profile), custom panels for new buffer types, and ensuring accurate rendering of ATProtoSocial posts and notifications. * **Completing Documentation:** Updating all relevant documentation files in `doc/` and `documentation/`. * **Updating Translations:** Adding new strings and updating translation files. * **Adding Tests:** Creating unit and integration tests for the new protocol. I was not stuck on any particular point, but the UI integration is a large step that requires iterative refinement and testing for each component, which would naturally extend beyond a single development cycle for a feature of this scope.
2025-05-26 14:11:01 +00:00
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any
fromapprove.translation import translate as _
if TYPE_CHECKING:
fromapprove.sessions.atprotosocial.session import Session as ATProtoSocialSession
logger = logging.getLogger(__name__)
class ATProtoSocialCompose:
# Maximum number of characters allowed in a post on ATProtoSocial (Bluesky uses graphemes, not codepoints)
# Bluesky's limit is 300 graphemes. This might need adjustment based on how Python handles graphemes.
MAX_CHARS = 300 # Defined by app.bsky.feed.post schema (description for text field)
MAX_MEDIA_ATTACHMENTS = 4 # Defined by app.bsky.embed.images schema (maxItems for images array)
MAX_LANGUAGES = 3 # Defined by app.bsky.feed.post schema (maxItems for langs array)
# MAX_POLL_OPTIONS = 4 # Polls are not yet standard in ATProto, but some clients support them.
# MAX_POLL_OPTION_CHARS = 25
# MIN_POLL_DURATION = 5 * 60 # 5 minutes
# MAX_POLL_DURATION = 7 * 24 * 60 * 60 # 7 days
# Bluesky image size limit is 1MB (1,000,000 bytes)
# https://github.com/bluesky-social/social-app/blob/main/src/lib/constants.ts#L28
MAX_IMAGE_SIZE_BYTES = 1_000_000
def __init__(self, session: ATProtoSocialSession) -> 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."""
return {
"max_chars": self.MAX_CHARS,
"max_media_attachments": self.MAX_MEDIA_ATTACHMENTS,
"supports_content_warning": True, # Bluesky uses self-labels for content warnings
"supports_scheduled_posts": False, # ATProto/Bluesky does not natively support scheduled posts
"supported_media_types": self.supported_media_types,
"max_media_size_bytes": self.max_image_size_bytes,
"supports_alternative_text": True, # Alt text is supported for images
"sensitive_reasons_options": self.session.get_sensitive_reason_options(), # For self-labeling
"supports_language_selection": True, # app.bsky.feed.post supports 'langs' field
"max_languages": self.MAX_LANGUAGES,
"supports_quoting": True, # Bluesky supports quoting via app.bsky.embed.record
"supports_polls": False, # No standard poll support in ATProto yet
# "max_poll_options": self.MAX_POLL_OPTIONS,
# "max_poll_option_chars": self.MAX_POLL_OPTION_CHARS,
# "min_poll_duration": self.MIN_POLL_DURATION,
# "max_poll_duration": self.MAX_POLL_DURATION,
}
async def get_quote_text(self, message_id: str, url: str) -> str | None:
"""
Generates text to be added to the compose box when quoting an ATProtoSocial post.
For Bluesky, the actual quote is an embed. This text is typically appended by the user.
`message_id` here is the AT-URI of the post to be quoted.
`url` is the web URL of the post.
"""
# The actual embedding of a quote is handled in session.send_message by passing quote_uri.
# This method is for any text that might be automatically added to the *user's post text*.
# Often, users just add the link manually, or clients might add "QT: [link]".
# For now, returning an empty string means no text is automatically added to the compose box,
# the UI will handle showing the quote embed and the user types their own commentary.
# Alternatively, return `url` if the desired behavior is to paste the URL into the text.
# Example: Fetching post details to include a snippet (can be slow)
# try:
# post_view = await self.session.util.get_post_by_uri(message_id) # Assuming message_id is AT URI
# if post_view and post_view.author and post_view.record:
# author_handle = post_view.author.handle
# text_snippet = str(post_view.record.text)[:70] # Take first 70 chars of post text
# return f"QT @{author_handle}: \"{text_snippet}...\"\n{url}\n"
# except Exception as e:
# logger.warning(f"Could not fetch post for quote text ({message_id}): {e}")
# return f"{url} " # Just the URL, or empty string
return "" # No automatic text added; UI handles visual quote, user adds own text.
async def get_reply_text(self, message_id: str, author_handle: str) -> str | None:
"""
Generates reply text (mention) for a given author handle for ATProtoSocial.
"""
# TODO: Confirm if any specific prefix is needed beyond the mention.
# Bluesky handles mentions with "@handle.example.com"
if not author_handle.startswith("@"):
return f"@{author_handle} "
return f"{author_handle} "
# Any other ATProtoSocial specific compose methods would go here.
# For example, methods to handle draft creation, media uploads prior to posting, etc.
# async def upload_media(self, file_path: str, mime_type: str, description: str | None = None) -> dict[str, Any] | None:
# """
# Uploads a media file to ATProtoSocial and returns media ID or details.
# This would use the atproto client's blob upload.
# """
# # try:
# # # client = self.session.util.get_client() # Assuming a method to get an authenticated atproto client
# # with open(file_path, "rb") as f:
# # blob_data = f.read()
# # # response = await client.com.atproto.repo.upload_blob(blob_data, mime_type=mime_type)
# # # return {"id": response.blob.ref, "url": response.blob.cid, "description": description} # Example structure
# # logger.info(f"Media uploaded: {file_path}")
# # return {"id": "fake_media_id", "url": "fake_media_url", "description": description} # Placeholder
# # except Exception as e:
# # logger.error(f"Failed to upload media to ATProtoSocial: {e}")
# # return None
# pass
def get_text_formatting_rules(self) -> dict[str, Any]:
"""
Returns text formatting rules for ATProtoSocial.
Bluesky uses Markdown for rich text, but it's processed server-side from facets.
Client-side, users type plain text and the client detects links, mentions, etc., to create facets.
"""
return {
"markdown_enabled": False, # Users type plain text; facets are for rich text features
"custom_emojis_enabled": False, # ATProto doesn't have custom emojis like Mastodon
"max_length": self.MAX_CHARS,
"line_break_char": "\n",
# Information about how links, mentions, tags are formatted or if they count towards char limit differently
"link_format": "Full URL (e.g., https://example.com)", # Links are typically full URLs
"mention_format": "@handle.bsky.social",
"tag_format": "#tag (becomes a facet link)", # Hashtags are detected and become links
}
def is_media_type_supported(self, mime_type: str) -> bool:
"""Checks if a given MIME type is supported for upload."""
# TODO: Use actual supported types from `self.supported_media_types`
return mime_type.lower() in self.supported_media_types
def get_max_schedule_date(self) -> str | None:
"""Returns the maximum date posts can be scheduled to, if supported."""
# ATProtoSocial does not natively support scheduled posts.
return None
def get_poll_configuration(self) -> dict[str, Any] | None:
"""Returns configuration for polls, if supported."""
# Polls are not a standard part of ATProto yet.
# If implementing client-side polls or if official support arrives, this can be updated.
# return {
# "max_options": self.MAX_POLL_OPTIONS,
# "max_option_chars": self.MAX_POLL_OPTION_CHARS,
# "min_duration_seconds": self.MIN_POLL_DURATION,
# "max_duration_seconds": self.MAX_POLL_DURATION,
# "default_duration_seconds": 24 * 60 * 60, # 1 day
# }
return None