"""Greengale blog post creation tool. Creates blog posts on Greengale (https://greengale.app), an ATProto-based blogging service. Posts are created using the app.greengale.blog.entry lexicon and served at greengale.app. """ from typing import Optional from pydantic import BaseModel, Field class BlogPostCreateArgs(BaseModel): title: str = Field( ..., description="Title of the blog post" ) content: str = Field( ..., description="Main content of the blog post (Markdown supported)" ) subtitle: Optional[str] = Field( default=None, description="Optional subtitle for the blog post" ) def blog_post_create(title: str, content: str, subtitle: Optional[str] = None) -> str: """ Create a new blog post on Greengale. This tool creates blog posts using the app.greengale.blog.entry lexicon on the ATProto network. Blog posts are publicly visible and use the github-light theme. The blog post will be served at: https://greengale.app/{handle}/{post_id} Args: title: Title of the blog post content: Main content of the blog post (Markdown supported) subtitle: Optional subtitle for the blog post Returns: Success message with the blog post URL on greengale.app Raises: Exception: If the post creation fails """ import os import requests from datetime import datetime, timezone try: # Get credentials from environment username = os.getenv("BSKY_USERNAME") password = os.getenv("BSKY_PASSWORD") pds_host = os.getenv("PDS_URI", "https://bsky.social") if not username or not password: raise Exception("BSKY_USERNAME and BSKY_PASSWORD environment variables must be set") # Create session session_url = f"{pds_host}/xrpc/com.atproto.server.createSession" session_data = { "identifier": username, "password": password } session_response = requests.post(session_url, json=session_data, timeout=10) session_response.raise_for_status() session = session_response.json() access_token = session.get("accessJwt") user_did = session.get("did") handle = session.get("handle", username) if not access_token or not user_did: raise Exception("Failed to get access token or DID from session") # Create blog post record now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") blog_record = { "$type": "app.greengale.blog.entry", "theme": "github-light", "title": title, "content": content, "createdAt": now, "visibility": "public" } # Add subtitle if provided if subtitle: blog_record["subtitle"] = subtitle # Create the record headers = {"Authorization": f"Bearer {access_token}"} create_record_url = f"{pds_host}/xrpc/com.atproto.repo.createRecord" create_data = { "repo": user_did, "collection": "app.greengale.blog.entry", "record": blog_record } post_response = requests.post(create_record_url, headers=headers, json=create_data, timeout=10) post_response.raise_for_status() result = post_response.json() # Extract the record key from the URI post_uri = result.get("uri") if post_uri: rkey = post_uri.split("/")[-1] # Construct the Greengale blog URL blog_url = f"https://greengale.app/{handle}/{rkey}" else: blog_url = "URL generation failed" # Build success message success_parts = [ f"Successfully created blog post on Greengale!", f"Title: {title}" ] if subtitle: success_parts.append(f"Subtitle: {subtitle}") success_parts.extend([ f"URL: {blog_url}", f"Theme: github-light", f"Visibility: public" ]) return "\n".join(success_parts) except Exception as e: raise Exception(f"Error creating blog post: {str(e)}")