From 0aa5818159ba19f983c983467a6ec8c268a85167 Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Wed, 16 Jul 2025 08:38:57 -0700 Subject: [PATCH] Add Whitewind blog post creation tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create new tool for posting to Whitewind using com.whtwnd.blog.entry lexicon - Support title, content (markdown), and optional subtitle - Hardcode theme as github-light and visibility as public - Generate proper Whitewind URLs (https://whtwnd.com/{handle}/entries/{rkey}) - Follow existing pattern of using environment variables for credentials 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- tools/__init__.py | 3 ++ tools/whitewind.py | 124 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 tools/whitewind.py diff --git a/tools/__init__.py b/tools/__init__.py index 300acb6..9dba129 100644 --- a/tools/__init__.py +++ b/tools/__init__.py @@ -4,6 +4,7 @@ from .search import search_bluesky_posts, SearchArgs from .post import create_new_bluesky_post, PostArgs from .feed import get_bluesky_feed, FeedArgs from .blocks import attach_user_blocks, detach_user_blocks, AttachUserBlocksArgs, DetachUserBlocksArgs +from .whitewind import create_whitewind_blog_post, WhitewindPostArgs __all__ = [ # Functions @@ -12,10 +13,12 @@ __all__ = [ "get_bluesky_feed", "attach_user_blocks", "detach_user_blocks", + "create_whitewind_blog_post", # Pydantic models "SearchArgs", "PostArgs", "FeedArgs", "AttachUserBlocksArgs", "DetachUserBlocksArgs", + "WhitewindPostArgs", ] \ No newline at end of file diff --git a/tools/whitewind.py b/tools/whitewind.py new file mode 100644 index 0000000..ad6950e --- /dev/null +++ b/tools/whitewind.py @@ -0,0 +1,124 @@ +"""Whitewind blog post creation tool.""" +from typing import Optional +from pydantic import BaseModel, Field + + +class WhitewindPostArgs(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 create_whitewind_blog_post(title: str, content: str, subtitle: Optional[str] = None) -> str: + """ + Create a new blog post on Whitewind. + + This tool creates blog posts using the com.whtwnd.blog.entry lexicon on the ATProto network. + The posts are publicly visible and use the github-light theme. + + 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 + + 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": "com.whtwnd.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": "com.whtwnd.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 Whitewind blog URL + blog_url = f"https://whtwnd.com/{handle}/entries/{rkey}" + else: + blog_url = "URL generation failed" + + # Build success message + success_parts = [ + f"Successfully created Whitewind blog post!", + 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 Whitewind blog post: {str(e)}") \ No newline at end of file -- 2.51.2