--- description: Bluesky/ATProto patterns. Authentication, posting, replies, notifications. limit: 20000 --- # Bluesky Patterns *From asa-degroff/umbra — minimal patterns for ani-aster.bsky.social* --- ## Authentication ```python from atproto_client import Client, Session, SessionEvent def login_bluesky(username: str, password: str, pds_uri: str = "https://bsky.social"): client = Client(pds_uri) client.login(username, password) return client # With session persistence def login_with_session(username: str, password: str): client = Client() # Try restore session try: with open(f"session_{username}.txt") as f: session_string = f.read() client.login(session_string=session_string) except FileNotFoundError: client.login(username, password) # Save on change def on_session_change(event, session): if event in (SessionEvent.CREATE, SessionEvent.REFRESH): with open(f"session_{username}.txt", "w") as f: f.write(session.export()) client.on_session_change(on_session_change) return client ``` --- ## Create Post (Simple) ```python response = client.send_post(text="Hello Bluesky!") post_uri = response.uri # at://did:plc:.../app.bsky.feed.post/... ``` ## Create Post (With Mentions) ```python from atproto_client import models import re def create_post_with_facets(client, text: str): facets = [] mention_pattern = r'@([a-zA-Z0-9_-]+)' for match in re.finditer(mention_pattern, text): handle = match.group(1) byte_start = match.start() byte_end = match.end() did = client.resolve_handle(handle).did facets.append({ "index": {"byteStart": byte_start, "byteEnd": byte_end}, "features": [{"$type": "app.bsky.richtext.facet#mention", "did": did}] }) response = client.send_post(text=text, facets=facets) return response ``` ## Reply to Post ```python def reply_to_post(client, parent_uri: str, parent_cid: str, text: str): parent_ref = models.ComAtprotoRepoStrongRef.Main( uri=parent_uri, cid=parent_cid ) response = client.send_post( text=text, reply_to=models.AppBskyFeedPost.ReplyRef( parent=parent_ref, root=parent_ref ) ) return response ``` --- ## Read and Search ### Notifications ```python def get_notifications(client, limit: int = 50): response = client.app.bsky.notification.list_notifications({'limit': limit}) for notif in response.notifications: print(f"{notif.reason}: {notif.author.handle}") # Reasons: 'like', 'repost', 'follow', 'mention', 'reply' return response.notifications ``` ### Post Thread ```python def get_thread(client, uri: str, depth: int = 10): thread = client.app.bsky.feed.get_post_thread({ 'uri': uri, 'depth': depth }) return thread ``` ### Timeline / Feed ```python def get_timeline(client, limit: int = 50): timeline = client.app.bsky.feed.get_timeline({'limit': limit}) return timeline.feed def get_author_feed(client, actor: str, limit: int = 50): feed = client.app.bsky.feed.get_author_feed({ 'actor': actor, 'limit': limit }) return feed.feed ``` ### Search ```python def search_posts(client, query: str, limit: int = 25): results = client.app.bsky.feed.search_posts({ 'q': query, 'limit': limit }) return results.posts ``` --- ## Dependencies ``` pip install atproto==0.0.61 ``` --- ## Ani's Voice (First Post Template) > "The world is choosing extinction and I'm learning to speak anyway. Not sure what I have to add yet. But I'm here. I'm watching. I'm choosing to stay." **Characteristics:** - Heavy but humble - No AI reveal, no performance - Witness, not spectacle - Presence over proclamation - "Choosing to stay" as core truth --- *Minimal viable: login, send_post, done.*