this repo has no description

fix up examples

+281 -61
+1
.gitignore
··· 1 .env
··· 1 .env 2 + node_modules/
+51
ACTIVITIES.md
···
··· 1 + # Activity Overview 2 + 3 + ## What We Built 4 + 5 + ### 1. Basic Posting (01a-hello-world.js) 6 + - Connects to Bluesky using app password 7 + - Posts a simple message with #ATProtoChicago 8 + - Shows the basics of authentication and posting 9 + 10 + ### 2. Creative Posts (01b-emoji-art.js) 11 + - Generates random emoji art grids 12 + - Demonstrates creative uses of the posting API 13 + - Fun visual output that's shareable 14 + 15 + ### 3. Rich Text Features (02-rich-text.js) 16 + - Auto-detects links and converts them to clickable links 17 + - Auto-detects hashtags for discoverability 18 + - Shows how Bluesky enhances plain text 19 + 20 + ### 4. Real-time Firehose (03-feed-generator.js) 21 + - Connects to Bluesky's Jetstream service 22 + - Filters the entire network for #ATProtoChicago posts 23 + - Shows posts in real-time with clean JSON format 24 + - Demonstrates how feed generators work 25 + 26 + ### 5. Timeline Filtering (05-post-filter.js) 27 + - Fetches your home timeline 28 + - Filters for Chicago/ATProto mentions 29 + - Shows engagement metrics 30 + - Local processing example 31 + 32 + ### 6. Profile Updates (04-profile-updater.js) 33 + - Programmatically updates your profile 34 + - Adds timestamp to description 35 + - Shows how to modify account data 36 + 37 + ## Key Concepts Demonstrated 38 + 39 + - **Authentication**: Using app passwords safely 40 + - **CRUD Operations**: Create posts, read timeline, update profile 41 + - **Real-time Data**: Consuming the firehose 42 + - **Data Processing**: Filtering and analyzing posts 43 + - **Rich Content**: Links, mentions, hashtags 44 + - **API Patterns**: Consistent error handling and async/await 45 + 46 + ## Architecture Notes 47 + 48 + - All examples use the official `@atproto/api` SDK 49 + - Jetstream provides JSON instead of binary CAR/CBOR 50 + - Credentials stored in `.env` for security 51 + - Bun runtime for fast execution and built-in TypeScript support
+26 -5
README.md
··· 7 1. **Create a Bluesky account** at https://bsky.app 8 2. **Generate an app password**: Settings → App Passwords → Add 9 3. **Install Bun** (if needed): `curl -fsSL https://bun.sh/install | bash` 10 - 4. **Clone this repo** and create `.env` file: 11 ``` 12 BSKY_USERNAME=your-handle.bsky.social 13 BSKY_PASSWORD=xxxx-xxxx-xxxx-xxxx ··· 33 ``` 34 Demonstrates auto-detection of links and hashtags in posts. 35 36 - ### Activity 3: Feed Generator Metadata (15 minutes) 37 ```bash 38 bun activities/03-feed-generator.js 39 ``` 40 - Creates metadata for a custom feed (note: actual feed requires a server). 41 42 - ### Activity 4: Profile Updater (10 minutes) 43 ```bash 44 bun activities/04-profile-updater.js 45 ``` ··· 60 - Use `#ATProtoChicago` to find other meetup participants! 61 62 ## Common Issues 63 - **Rate limits**: The API has rate limits, wait a minute if you hit them 64 - - **Module not found**: Run the scripts from the repo root directory
··· 7 1. **Create a Bluesky account** at https://bsky.app 8 2. **Generate an app password**: Settings → App Passwords → Add 9 3. **Install Bun** (if needed): `curl -fsSL https://bun.sh/install | bash` 10 + 4. **Clone this repo** and install dependencies: 11 + ```bash 12 + git clone <this-repo> 13 + cd atproto-meetup 14 + bun install 15 + ``` 16 + 5. **Create `.env` file** with your credentials: 17 ``` 18 BSKY_USERNAME=your-handle.bsky.social 19 BSKY_PASSWORD=xxxx-xxxx-xxxx-xxxx ··· 39 ``` 40 Demonstrates auto-detection of links and hashtags in posts. 41 42 + ### Activity 3: Real-time Firehose with Jetstream (15 minutes) 43 ```bash 44 bun activities/03-feed-generator.js 45 ``` 46 + Connect to Bluesky's Jetstream service to see real-time posts with #ATProtoChicago. Uses simple JSON instead of complex binary formats! 47 + 48 + ### Activity 4: Local Post Filter (15 minutes) 49 + ```bash 50 + bun activities/05-post-filter.js 51 + ``` 52 + Build a local feed algorithm that filters your timeline for Chicago/ATProto posts. 53 54 + ### Activity 5: Profile Updater (10 minutes) 55 ```bash 56 bun activities/04-profile-updater.js 57 ``` ··· 72 - Use `#ATProtoChicago` to find other meetup participants! 73 74 ## Common Issues 75 + 76 + - **"Invalid identifier or password"**: Make sure you're using an app password, not your main password 77 - **Rate limits**: The API has rate limits, wait a minute if you hit them 78 + - **Module not found**: Run the scripts from the repo root directory 79 + - **WebSocket errors**: The firehose can be flaky, just restart if it disconnects 80 + 81 + ## What's Next? 82 + 83 + - Build a full feed generator: https://github.com/bluesky-social/feed-generator 84 + - Explore the AT Protocol specs: https://atproto.com/specs 85 + - Join the Bluesky developer community: https://github.com/bluesky-social/atproto/discussions
+3 -3
activities/01a-hello-world.js
··· 1 #!/usr/bin/env bun 2 - import { BskyAgent } from '@atproto/api' 3 4 // Read credentials from .env file 5 const username = process.env.BSKY_USERNAME ··· 10 process.exit(1) 11 } 12 13 - const agent = new BskyAgent({ service: 'https://bsky.social' }) 14 15 console.log('Logging in...') 16 await agent.login({ ··· 21 console.log('Connected! Your DID:', agent.session?.did) 22 23 const post = await agent.post({ 24 - text: 'Hello from the Chicago ATProto meetup! 🚀' 25 }) 26 27 console.log('Posted successfully!')
··· 1 #!/usr/bin/env bun 2 + import { AtpAgent } from '@atproto/api' 3 4 // Read credentials from .env file 5 const username = process.env.BSKY_USERNAME ··· 10 process.exit(1) 11 } 12 13 + const agent = new AtpAgent({ service: 'https://bsky.social' }) 14 15 console.log('Logging in...') 16 await agent.login({ ··· 21 console.log('Connected! Your DID:', agent.session?.did) 22 23 const post = await agent.post({ 24 + text: 'Hello from the #ATProtoChicago meetup! 🚀' 25 }) 26 27 console.log('Posted successfully!')
+2 -2
activities/01b-emoji-art.js
··· 1 #!/usr/bin/env bun 2 - import { BskyAgent } from '@atproto/api' 3 4 // Read credentials from .env 5 const username = process.env.BSKY_USERNAME ··· 10 process.exit(1) 11 } 12 13 - const agent = new BskyAgent({ service: 'https://bsky.social' }) 14 15 console.log('Logging in...') 16 await agent.login({
··· 1 #!/usr/bin/env bun 2 + import { AtpAgent } from '@atproto/api' 3 4 // Read credentials from .env 5 const username = process.env.BSKY_USERNAME ··· 10 process.exit(1) 11 } 12 13 + const agent = new AtpAgent({ service: 'https://bsky.social' }) 14 15 console.log('Logging in...') 16 await agent.login({
+2 -2
activities/02-rich-text.js
··· 1 #!/usr/bin/env bun 2 - import { BskyAgent, RichText } from '@atproto/api' 3 4 // Read credentials from .env 5 const username = process.env.BSKY_USERNAME ··· 10 process.exit(1) 11 } 12 13 - const agent = new BskyAgent({ service: 'https://bsky.social' }) 14 15 console.log('Logging in...') 16 await agent.login({
··· 1 #!/usr/bin/env bun 2 + import { AtpAgent, RichText } from '@atproto/api' 3 4 // Read credentials from .env 5 const username = process.env.BSKY_USERNAME ··· 10 process.exit(1) 11 } 12 13 + const agent = new AtpAgent({ service: 'https://bsky.social' }) 14 15 console.log('Logging in...') 16 await agent.login({
+110 -45
activities/03-feed-generator.js
··· 1 #!/usr/bin/env bun 2 - import { BskyAgent } from '@atproto/api' 3 4 // Read credentials from .env 5 const username = process.env.BSKY_USERNAME ··· 10 process.exit(1) 11 } 12 13 - const agent = new BskyAgent({ service: 'https://bsky.social' }) 14 15 console.log('Logging in...') 16 await agent.login({ ··· 20 21 console.log('Connected! Your DID:', agent.session?.did) 22 23 - // Create a simple feed generator record 24 - // Note: This creates the metadata for a feed, but you'd need a server to actually generate the feed content 25 - const feedRecord = { 26 - did: `did:plc:${agent.session.did.split(':')[2]}`, // Use your actual DID 27 - displayName: 'Chicago ATProto Meetup', 28 - description: 'Posts from our Chicago ATProto meetup! Tag your posts with #ATProtoChicago', 29 - avatar: undefined, 30 - createdAt: new Date().toISOString() 31 - } 32 33 - const rkey = 'chicago-meetup-feed' 34 35 - console.log('\nCreating feed generator record...') 36 - console.log('Feed details:', JSON.stringify(feedRecord, null, 2)) 37 38 - try { 39 - // First check if it already exists 40 - const existing = await agent.com.atproto.repo.getRecord({ 41 - repo: agent.session.did, 42 - collection: 'app.bsky.feed.generator', 43 - rkey: rkey 44 - }).catch(() => null) 45 46 - if (existing) { 47 - console.log('\nFeed already exists! Updating...') 48 - await agent.com.atproto.repo.putRecord({ 49 - repo: agent.session.did, 50 - collection: 'app.bsky.feed.generator', 51 - rkey: rkey, 52 - record: feedRecord, 53 - swapRecord: existing.data.cid 54 - }) 55 - } else { 56 - await agent.com.atproto.repo.putRecord({ 57 - repo: agent.session.did, 58 - collection: 'app.bsky.feed.generator', 59 - rkey: rkey, 60 - record: feedRecord 61 }) 62 } 63 64 - console.log('\nFeed generator record created successfully!') 65 - console.log(`\nNote: This creates the feed metadata. To make it functional, you would need:`) 66 - console.log('1. A server running the feed generation logic') 67 - console.log('2. The feed to be published and indexed by Bluesky') 68 - console.log(`\nYour feed URI: at://${agent.session.did}/app.bsky.feed.generator/${rkey}`) 69 - 70 - } catch (error) { 71 - console.error('Error creating feed:', error.message) 72 - }
··· 1 #!/usr/bin/env bun 2 + import { AtpAgent } from '@atproto/api' 3 + import { WebSocket } from 'ws' 4 5 // Read credentials from .env 6 const username = process.env.BSKY_USERNAME ··· 11 process.exit(1) 12 } 13 14 + const agent = new AtpAgent({ service: 'https://bsky.social' }) 15 16 console.log('Logging in...') 17 await agent.login({ ··· 21 22 console.log('Connected! Your DID:', agent.session?.did) 23 24 + // Store found posts 25 + let postCount = 0 26 + const foundPosts = [] 27 + 28 + console.log('\n🚀 Starting Feed Generator with Jetstream') 29 + console.log('==========================================') 30 + console.log('This connects to Bluesky\'s Jetstream service which provides:') 31 + console.log('- Simple JSON format (not binary CAR/CBOR)') 32 + console.log('- Filtered streams (just posts, not all events)') 33 + console.log('- Clean, readable data') 34 + console.log('\nLooking for #ATProtoChicago posts...') 35 + console.log('Press Ctrl+C to stop\n') 36 + 37 + // Connect to Jetstream - only get posts 38 + const ws = new WebSocket('wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post') 39 40 + ws.on('open', () => { 41 + console.log('✅ Connected to Jetstream!') 42 + console.log('Listening for posts...\n') 43 + console.log('═'.repeat(60)) 44 + }) 45 46 + ws.on('message', async (data) => { 47 + try { 48 + const event = JSON.parse(data.toString()) 49 + 50 + // Check if this is a post event 51 + if (event.commit && event.commit.record && event.commit.record.text) { 52 + const text = event.commit.record.text.toLowerCase() 53 + 54 + // Check for our hashtag 55 + if (text.includes('#atprotochicago') || (text.includes('atproto') && text.includes('chicago'))) { 56 + postCount++ 57 + const timestamp = new Date().toLocaleTimeString() 58 + 59 + console.log(`\n🎯 [${timestamp}] Found post #${postCount} with #ATProtoChicago!`) 60 + console.log('─'.repeat(60)) 61 + 62 + // Get author info from the event 63 + const did = event.did 64 + let authorHandle = did 65 + 66 + try { 67 + const profile = await agent.getProfile({ actor: did }) 68 + authorHandle = profile.data.handle 69 + if (profile.data.displayName) { 70 + authorHandle = `${profile.data.displayName} (@${profile.data.handle})` 71 + } 72 + } catch { 73 + // If profile lookup fails, just show DID 74 + authorHandle = did.substring(0, 20) + '...' 75 + } 76 + 77 + console.log(`👤 Author: ${authorHandle}`) 78 + console.log(`📝 Post:`) 79 + console.log('') 80 + 81 + // Display the post text 82 + const lines = event.commit.record.text.split('\n') 83 + lines.forEach(line => { 84 + console.log(` ${line}`) 85 + }) 86 + 87 + // Show metadata 88 + console.log('') 89 + console.log(`🔗 URI: at://${did}/app.bsky.feed.post/${event.commit.rkey}`) 90 + console.log(`🕐 Created: ${new Date(event.commit.record.createdAt).toLocaleString()}`) 91 + 92 + // Show engagement if available 93 + if (event.commit.record.reply) { 94 + console.log(`💬 This is a reply`) 95 + } 96 + 97 + console.log('─'.repeat(60)) 98 + console.log(`\n📊 Total posts found: ${postCount}`) 99 + console.log('═'.repeat(60)) 100 + 101 + foundPosts.push({ 102 + timestamp: new Date(), 103 + author: authorHandle, 104 + text: event.commit.record.text, 105 + uri: `at://${did}/app.bsky.feed.post/${event.commit.rkey}`, 106 + did: did 107 + }) 108 + } 109 + } 110 + } catch (error) { 111 + // Ignore parsing errors 112 + } 113 + }) 114 115 + ws.on('error', (error) => { 116 + console.error('\n❌ WebSocket error:', error.message) 117 + }) 118 119 + ws.on('close', () => { 120 + console.log('\n\n👋 Disconnected from Jetstream') 121 + console.log(`Found ${postCount} posts with #ATProtoChicago`) 122 + 123 + if (foundPosts.length > 0) { 124 + console.log('\n📋 Summary of posts found:') 125 + console.log('─'.repeat(60)) 126 + foundPosts.forEach((post, i) => { 127 + console.log(`${i + 1}. ${post.author}: "${post.text.substring(0, 50)}${post.text.length > 50 ? '...' : ''}"`) 128 }) 129 } 130 + }) 131 132 + // Graceful shutdown 133 + process.on('SIGINT', () => { 134 + console.log('\n\nShutting down gracefully...') 135 + ws.close() 136 + process.exit(0) 137 + })
+2 -2
activities/04-profile-updater.js
··· 1 #!/usr/bin/env bun 2 - import { BskyAgent } from '@atproto/api' 3 4 // Read credentials from .env 5 const username = process.env.BSKY_USERNAME ··· 10 process.exit(1) 11 } 12 13 - const agent = new BskyAgent({ service: 'https://bsky.social' }) 14 15 console.log('Logging in...') 16 await agent.login({
··· 1 #!/usr/bin/env bun 2 + import { AtpAgent } from '@atproto/api' 3 4 // Read credentials from .env 5 const username = process.env.BSKY_USERNAME ··· 10 process.exit(1) 11 } 12 13 + const agent = new AtpAgent({ service: 'https://bsky.social' }) 14 15 console.log('Logging in...') 16 await agent.login({
+72
activities/05-post-filter.js
···
··· 1 + #!/usr/bin/env bun 2 + import { AtpAgent } from '@atproto/api' 3 + 4 + // Read credentials from .env 5 + const username = process.env.BSKY_USERNAME 6 + const password = process.env.BSKY_PASSWORD 7 + 8 + if (!username || !password) { 9 + console.error('Please provide BSKY_USERNAME and BSKY_PASSWORD in .env file') 10 + process.exit(1) 11 + } 12 + 13 + const agent = new AtpAgent({ service: 'https://bsky.social' }) 14 + 15 + console.log('Logging in...') 16 + await agent.login({ 17 + identifier: username, 18 + password: password 19 + }) 20 + 21 + console.log('Fetching timeline...\n') 22 + 23 + // Get the user's timeline 24 + const timeline = await agent.getTimeline({ limit: 50 }) 25 + 26 + // Filter posts that mention Chicago or ATProto 27 + const chicagoPosts = timeline.data.feed.filter(item => { 28 + const text = item.post.record?.text?.toLowerCase() || '' 29 + return text.includes('chicago') || 30 + text.includes('atproto') || 31 + text.includes('#atprotochicago') 32 + }) 33 + 34 + console.log(`Found ${chicagoPosts.length} posts mentioning Chicago/ATProto out of ${timeline.data.feed.length} recent posts:\n`) 35 + 36 + if (chicagoPosts.length === 0) { 37 + console.log('No matching posts found. Try posting with #ATProtoChicago!') 38 + } else { 39 + chicagoPosts.forEach((item, index) => { 40 + const author = item.post.author.displayName || item.post.author.handle 41 + const text = item.post.record?.text || '' 42 + const time = new Date(item.post.record?.createdAt).toLocaleString() 43 + 44 + console.log(`${index + 1}. ${author} (${time})`) 45 + console.log(` ${text.substring(0, 100)}${text.length > 100 ? '...' : ''}`) 46 + console.log(` Likes: ${item.post.likeCount || 0}, Reposts: ${item.post.repostCount || 0}`) 47 + console.log('') 48 + }) 49 + } 50 + 51 + // Bonus: Show posts with the most engagement 52 + console.log('\n📊 Bonus: Top 5 posts by engagement:') 53 + console.log('=====================================') 54 + 55 + const sortedByEngagement = timeline.data.feed 56 + .sort((a, b) => { 57 + const aEngagement = (a.post.likeCount || 0) + (a.post.repostCount || 0) * 2 58 + const bEngagement = (b.post.likeCount || 0) + (b.post.repostCount || 0) * 2 59 + return bEngagement - aEngagement 60 + }) 61 + .slice(0, 5) 62 + 63 + sortedByEngagement.forEach((item, index) => { 64 + const author = item.post.author.displayName || item.post.author.handle 65 + const text = item.post.record?.text || '' 66 + const engagement = (item.post.likeCount || 0) + (item.post.repostCount || 0) * 2 67 + 68 + console.log(`${index + 1}. ${author} (score: ${engagement})`) 69 + console.log(` ${text.substring(0, 80)}${text.length > 80 ? '...' : ''}`) 70 + console.log(` Likes: ${item.post.likeCount || 0}, Reposts: ${item.post.repostCount || 0}`) 71 + console.log('') 72 + })
bun.lockb

This is a binary file and will not be displayed.

+10
package.json
···
··· 1 + { 2 + "name": "atproto-chicago-meetup", 3 + "version": "1.0.0", 4 + "description": "ATProto activities for Chicago Developer Meetup", 5 + "type": "module", 6 + "dependencies": { 7 + "@atproto/api": "^0.15.16", 8 + "ws": "^8.18.2" 9 + } 10 + }
+2 -2
starter-template.js
··· 1 #!/usr/bin/env bun 2 - import { BskyAgent } from '@atproto/api' 3 4 // Read credentials from .env file 5 const username = process.env.BSKY_USERNAME ··· 14 } 15 16 async function main() { 17 - const agent = new BskyAgent({ service: 'https://bsky.social' }) 18 19 await agent.login({ 20 identifier: username,
··· 1 #!/usr/bin/env bun 2 + import { AtpAgent } from '@atproto/api' 3 4 // Read credentials from .env file 5 const username = process.env.BSKY_USERNAME ··· 14 } 15 16 async function main() { 17 + const agent = new AtpAgent({ service: 'https://bsky.social' }) 18 19 await agent.login({ 20 identifier: username,