a digital person for bluesky
1"""Feed tool for retrieving Bluesky feeds."""
2from pydantic import BaseModel, Field
3from typing import Optional
4
5
6class FeedArgs(BaseModel):
7 feed_uri: Optional[str] = Field(None, description="Custom feed URI (e.g., 'at://did:plc:abc/app.bsky.feed.generator/feed-name'). If not provided, returns home timeline")
8 max_posts: int = Field(default=25, description="Maximum number of posts to retrieve (max 100)")
9
10
11def get_bluesky_feed(feed_uri: str = None, max_posts: int = 25) -> str:
12 """
13 Retrieve a Bluesky feed (home timeline or custom feed).
14
15 Args:
16 feed_uri: Custom feed URI (e.g., 'at://did:plc:abc/app.bsky.feed.generator/feed-name'). If not provided, returns home timeline
17 max_posts: Maximum number of posts to retrieve (max 100)
18
19 Returns:
20 YAML-formatted feed data with posts and metadata
21 """
22 import os
23 import yaml
24 import requests
25
26 try:
27 # Validate inputs
28 max_posts = min(max_posts, 100)
29
30 # Get credentials from environment
31 username = os.getenv("BSKY_USERNAME")
32 password = os.getenv("BSKY_PASSWORD")
33 pds_host = os.getenv("PDS_URI", "https://bsky.social")
34
35 if not username or not password:
36 raise Exception("BSKY_USERNAME and BSKY_PASSWORD environment variables must be set")
37
38 # Create session
39 session_url = f"{pds_host}/xrpc/com.atproto.server.createSession"
40 session_data = {
41 "identifier": username,
42 "password": password
43 }
44
45 try:
46 session_response = requests.post(session_url, json=session_data, timeout=10)
47 session_response.raise_for_status()
48 session = session_response.json()
49 access_token = session.get("accessJwt")
50
51 if not access_token:
52 raise Exception("Failed to get access token from session")
53 except Exception as e:
54 raise Exception(f"Authentication failed. ({str(e)})")
55
56 # Get feed
57 headers = {"Authorization": f"Bearer {access_token}"}
58
59 if feed_uri:
60 # Custom feed
61 feed_url = f"{pds_host}/xrpc/app.bsky.feed.getFeed"
62 params = {
63 "feed": feed_uri,
64 "limit": max_posts
65 }
66 feed_type = "custom"
67 feed_name = feed_uri.split('/')[-1] if '/' in feed_uri else feed_uri
68 else:
69 # Home timeline
70 feed_url = f"{pds_host}/xrpc/app.bsky.feed.getTimeline"
71 params = {
72 "limit": max_posts
73 }
74 feed_type = "home"
75 feed_name = "timeline"
76
77 try:
78 response = requests.get(feed_url, headers=headers, params=params, timeout=10)
79 response.raise_for_status()
80 feed_data = response.json()
81 except Exception as e:
82 raise Exception(f"Failed to get feed. ({str(e)})")
83
84 # Format posts
85 posts = []
86 for item in feed_data.get("feed", []):
87 post = item.get("post", {})
88 author = post.get("author", {})
89 record = post.get("record", {})
90
91 post_data = {
92 "author": {
93 "handle": author.get("handle", ""),
94 "display_name": author.get("displayName", ""),
95 },
96 "text": record.get("text", ""),
97 "created_at": record.get("createdAt", ""),
98 "uri": post.get("uri", ""),
99 "cid": post.get("cid", ""),
100 "like_count": post.get("likeCount", 0),
101 "repost_count": post.get("repostCount", 0),
102 "reply_count": post.get("replyCount", 0),
103 }
104
105 # Add repost info if present
106 if "reason" in item and item["reason"]:
107 reason = item["reason"]
108 if reason.get("$type") == "app.bsky.feed.defs#reasonRepost":
109 by = reason.get("by", {})
110 post_data["reposted_by"] = {
111 "handle": by.get("handle", ""),
112 "display_name": by.get("displayName", ""),
113 }
114
115 # Add reply info if present
116 if "reply" in record and record["reply"]:
117 parent = record["reply"].get("parent", {})
118 post_data["reply_to"] = {
119 "uri": parent.get("uri", ""),
120 "cid": parent.get("cid", ""),
121 }
122
123 posts.append(post_data)
124
125 # Format response
126 feed_result = {
127 "feed": {
128 "type": feed_type,
129 "name": feed_name,
130 "post_count": len(posts),
131 "posts": posts
132 }
133 }
134
135 if feed_uri:
136 feed_result["feed"]["uri"] = feed_uri
137
138 return yaml.dump(feed_result, default_flow_style=False, sort_keys=False)
139
140 except Exception as e:
141 raise Exception(f"Error retrieving feed: {str(e)}")