a digital person for bluesky
at 5d72e5d2729782a09059fe4ecb3cf8cdf59ea2e2 119 lines 4.5 kB view raw
1"""Post tool for creating Bluesky posts.""" 2from pydantic import BaseModel, Field 3 4 5class PostArgs(BaseModel): 6 text: str = Field(..., description="The text content to post (max 300 characters)") 7 8 9def post_to_bluesky(text: str) -> str: 10 """Post a message to Bluesky.""" 11 import os 12 import requests 13 from datetime import datetime, timezone 14 15 try: 16 # Validate character limit 17 if len(text) > 300: 18 raise Exception(f"Post exceeds 300 character limit (current: {len(text)} characters)") 19 20 # Get credentials from environment 21 username = os.getenv("BSKY_USERNAME") 22 password = os.getenv("BSKY_PASSWORD") 23 pds_host = os.getenv("PDS_URI", "https://bsky.social") 24 25 if not username or not password: 26 raise Exception("BSKY_USERNAME and BSKY_PASSWORD environment variables must be set") 27 28 # Create session 29 session_url = f"{pds_host}/xrpc/com.atproto.server.createSession" 30 session_data = { 31 "identifier": username, 32 "password": password 33 } 34 35 session_response = requests.post(session_url, json=session_data, timeout=10) 36 session_response.raise_for_status() 37 session = session_response.json() 38 access_token = session.get("accessJwt") 39 user_did = session.get("did") 40 41 if not access_token or not user_did: 42 raise Exception("Failed to get access token or DID from session") 43 44 # Build post record with facets for mentions and URLs 45 now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") 46 47 post_record = { 48 "$type": "app.bsky.feed.post", 49 "text": text, 50 "createdAt": now, 51 } 52 53 # Add facets for mentions and URLs 54 import re 55 facets = [] 56 57 # Parse mentions 58 mention_regex = rb"[$|\W](@([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)" 59 text_bytes = text.encode("UTF-8") 60 61 for m in re.finditer(mention_regex, text_bytes): 62 handle = m.group(1)[1:].decode("UTF-8") # Remove @ prefix 63 try: 64 resolve_resp = requests.get( 65 f"{pds_host}/xrpc/com.atproto.identity.resolveHandle", 66 params={"handle": handle}, 67 timeout=5 68 ) 69 if resolve_resp.status_code == 200: 70 did = resolve_resp.json()["did"] 71 facets.append({ 72 "index": { 73 "byteStart": m.start(1), 74 "byteEnd": m.end(1), 75 }, 76 "features": [{"$type": "app.bsky.richtext.facet#mention", "did": did}], 77 }) 78 except: 79 continue 80 81 # Parse URLs 82 url_regex = rb"[$|\W](https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*[-a-zA-Z0-9@%_\+~#//=])?)" 83 84 for m in re.finditer(url_regex, text_bytes): 85 url = m.group(1).decode("UTF-8") 86 facets.append({ 87 "index": { 88 "byteStart": m.start(1), 89 "byteEnd": m.end(1), 90 }, 91 "features": [{"$type": "app.bsky.richtext.facet#link", "uri": url}], 92 }) 93 94 if facets: 95 post_record["facets"] = facets 96 97 # Create the post 98 create_record_url = f"{pds_host}/xrpc/com.atproto.repo.createRecord" 99 headers = {"Authorization": f"Bearer {access_token}"} 100 101 create_data = { 102 "repo": user_did, 103 "collection": "app.bsky.feed.post", 104 "record": post_record 105 } 106 107 post_response = requests.post(create_record_url, headers=headers, json=create_data, timeout=10) 108 post_response.raise_for_status() 109 result = post_response.json() 110 111 post_uri = result.get("uri") 112 handle = session.get("handle", username) 113 rkey = post_uri.split("/")[-1] if post_uri else "" 114 post_url = f"https://bsky.app/profile/{handle}/post/{rkey}" 115 116 return f"Successfully posted to Bluesky!\nPost URL: {post_url}\nText: {text}" 117 118 except Exception as e: 119 raise Exception(f"Error posting to Bluesky: {str(e)}")